Skip to content

[security] Artifact broker keys omit org from upload namespace #702

Description

@coygeek

[security] Artifact broker keys omit org from upload namespace

Summary

Brokered artifact uploads are authorized with Crabbox's normal authenticated coordinator identity, which includes both owner and org, but the signed storage namespace includes only the owner and caller-selected artifact prefix. As a result, the same owner identity operating in two Crabbox org scopes can request upload grants for the same prefix and artifact name and receive signed PUT/read URLs for the same object key.

This crosses the supported Crabbox boundary for authenticated coordinator resource isolation. SECURITY.md treats access to another owner's resources contrary to documented authorization as in scope, and docs/security.md documents owner/org scoping for coordinator data and signed user tokens. The issue does not depend on hostile arbitrary tenants sharing one broker; it depends on a legitimate authenticated identity in one org colliding with artifacts expected to belong to another org.

Affected Components

  • Checked commit: 6dba4afd1c5a4be0a780cf035d1b397e8982d478
  • Component: coordinator artifact broker
  • Affected files:
    • worker/src/fleet.ts
    • worker/src/artifacts.ts
    • internal/cli/artifacts_publish.go
    • internal/cli/coordinator.go
    • worker/test/fleet.test.ts

Attack Path

Attacker role:

Authenticated user token or shared/operator token for an owner identity that can operate in a different Crabbox org scope.

Prerequisites:

  • The coordinator artifact broker is configured with CRABBOX_ARTIFACTS_BACKEND, bucket credentials, and either signed read URLs or CRABBOX_ARTIFACTS_BASE_URL.
  • The same owner identity can authenticate or be attributed in two org scopes that the deployment treats as separate Crabbox authorization scopes.
  • A victim org publishes artifacts at a reusable or observable broker prefix, or the attacker can predict the victim's artifact prefix and file name.

Steps:

  1. In org A, a victim publishes artifacts through the broker. The CLI sends POST /v1/artifacts/uploads with a prefix and file list, then uploads bytes through the returned signed PUT URLs.
  2. In org B, the attacker authenticates as the same owner identity and sends the same POST /v1/artifacts/uploads request with the same prefix and file name.
  3. createArtifactUploads calls artifactUploadResponse(this.env, input, requestOwner(request)), so the org from the authenticated request is not part of the signing context.
  4. artifactUploadResponse computes the storage prefix as artifactPrefix(config.prefix, owner, request.prefix), and artifactPrefix joins only the configured prefix, owner, and caller prefix.
  5. The returned upload grant targets the same object key for org A and org B. Uploading attacker-controlled bytes through the grant overwrites or spoofs the artifact bytes and read URL expected by the victim org.

Control/data flow:

authenticated request in org B
  -> createArtifactUploads authenticates normally but passes requestOwner only
  -> artifactUploadResponse builds configPrefix/owner/requestPrefix
  -> artifactObjectKey appends attacker-selected artifact name
  -> signed PUT and read URL collide with org A for the same owner/prefix/name

Impact

This is a cross-org artifact integrity issue with limited confidentiality impact in deployments that use the coordinator artifact broker. A user valid in one org can collide with artifacts generated for another org that shares the same owner identity because org is ignored in the signed object namespace, even though other coordinator resources and user tokens carry owner/org identity.

The practical outcome is that an attacker can replace or pre-create artifacts that another org expects to be isolated under its own Crabbox scope, such as screenshots, logs, manifests, or PR evidence published by brokered artifact workflows. If artifact URLs or manifests are shared across workflows, the same namespace collision can also expose one org's artifact URL/key material to another org for identical owner/prefix/name tuples.

Severity Assessment

CVSS Assessment

Metric v3.1 v4.0
Score 6.4 / 10.0 5.3 / 10.0
Severity Medium Medium
Vector CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:N CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:L/VA:N/SC:L/SI:L/SA:N
Calculator CVSS v3.1 Calculator CVSS v4.0 Calculator

The issue requires an authenticated Crabbox credential and a deployment that treats organizations as separate scopes, so it is not an unauthenticated or admin-impacting bug. It is still security-relevant because the coordinator signs write/read access to storage objects and the missing org segment enables cross-scope artifact spoofing or overwrite.

Recommended Remediation

Include the request organization in the brokered artifact namespace and make that tuple part of the server-side signing contract. A minimal fix is to pass both requestOwner(request) and requestOrg(request, this.env) from createArtifactUploads into artifactUploadResponse, then build keys using an unambiguous org+owner namespace such as configPrefix/org/owner/requestPrefix/name.

Normalize the org segment with the same path-part handling used for owner and prefix. Add a compatibility decision for existing owner-only artifact URLs, such as keeping old objects readable until expiry while signing all new grants under a versioned or org-aware prefix.

Validation

Validation method:

Source review against the isolated verification checkout at commit 6dba4afd1c5a4be0a780cf035d1b397e8982d478.

Evidence:

  • worker/src/coordinator-entry.ts authenticates non-health, non-agent coordinator routes before fleet handling and writes trusted x-crabbox-owner and x-crabbox-org headers from the auth context.
  • worker/src/auth.ts verifies signed user tokens carrying both owner and org, and requestWithAuthContext forwards both values to downstream fleet handlers.
  • worker/src/fleet.ts:8879-8884 handles POST /v1/artifacts/uploads, reads the authenticated JSON body, and calls artifactUploadResponse(this.env, input, requestOwner(request)); it does not pass requestOrg(request, this.env).
  • worker/src/artifacts.ts:57-68 accepts only owner as identity context and computes prefix = artifactPrefix(config.prefix, owner, request.prefix).
  • worker/src/artifacts.ts:191-213 builds the object key prefix from the configured prefix, owner, and caller-supplied request prefix, then appends the normalized file name.
  • worker/test/fleet.test.ts:16040-16075 asserts the current brokered key shape as qa/peter@example.com/pr-42/screenshots/after.png, demonstrating the org segment is absent from the signed key and read URL.
  • internal/cli/artifacts_publish.go:203-253 shows the CLI sends caller-selected Prefix and file Name values to the broker and trusts the returned Key and URL for publication.
  • internal/cli/coordinator.go:1644-1647 sends artifact upload requests to POST /v1/artifacts/uploads.

Counterevidence considered:

  • internal/cli/artifacts_publish.go:255-273 generates a timestamped default prefix when the user does not provide one, which reduces accidental collisions. It does not protect direct API clients, scripted publication with stable prefixes, or intentional reuse after an attacker observes a manifest or URL.
  • worker/src/artifacts.ts:173-180 rejects traversal in artifact names, so this is not arbitrary key traversal. The issue is that two otherwise valid org-scoped requests are signed into the same owner-only namespace.

Suggested regression coverage:

  • Add a worker/test/fleet.test.ts case that sends two authenticated POST /v1/artifacts/uploads requests with the same owner, prefix, and file name but different x-crabbox-org values, and assert the returned prefix, key, upload URL, and read URL differ.
  • Add focused worker/src/artifacts.ts unit coverage for the org-aware prefix builder, including path-like org names and empty/default-org behavior.

Verification command:

npm test --prefix worker -- fleet.test.ts

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Normal priority bug or improvement with limited blast radius.clawsweeper:fix-shape-clearClawSweeper found a clear likely implementation shape for this issue.clawsweeper:needs-maintainer-reviewClawSweeper marked this issue as needing maintainer review before automation.clawsweeper:needs-product-decisionClawSweeper marked this issue as needing a product or behavior decision.clawsweeper:needs-security-reviewClawSweeper marked this issue as needing security-sensitive review.clawsweeper:no-new-fix-prClawSweeper does not recommend queueing a new automated fix PR for this issue.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:securityThis issue is about security boundaries, credentials, authz, sandboxing, or sensitive data.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions