[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:
- 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.
- 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.
createArtifactUploads calls artifactUploadResponse(this.env, input, requestOwner(request)), so the org from the authenticated request is not part of the signing context.
artifactUploadResponse computes the storage prefix as artifactPrefix(config.prefix, owner, request.prefix), and artifactPrefix joins only the configured prefix, owner, and caller prefix.
- 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
[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
ownerandorg, 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 sameprefixand artifactnameand receive signed PUT/read URLs for the same object key.This crosses the supported Crabbox boundary for authenticated coordinator resource isolation.
SECURITY.mdtreats access to another owner's resources contrary to documented authorization as in scope, anddocs/security.mddocuments 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
6dba4afd1c5a4be0a780cf035d1b397e8982d478worker/src/fleet.tsworker/src/artifacts.tsinternal/cli/artifacts_publish.gointernal/cli/coordinator.goworker/test/fleet.test.tsAttack Path
Attacker role:
Authenticated user token or shared/operator token for an owner identity that can operate in a different Crabbox org scope.
Prerequisites:
CRABBOX_ARTIFACTS_BACKEND, bucket credentials, and either signed read URLs orCRABBOX_ARTIFACTS_BASE_URL.Steps:
POST /v1/artifacts/uploadswith aprefixand file list, then uploads bytes through the returned signed PUT URLs.POST /v1/artifacts/uploadsrequest with the sameprefixand filename.createArtifactUploadscallsartifactUploadResponse(this.env, input, requestOwner(request)), so the org from the authenticated request is not part of the signing context.artifactUploadResponsecomputes the storage prefix asartifactPrefix(config.prefix, owner, request.prefix), andartifactPrefixjoins only the configured prefix, owner, and caller prefix.Control/data flow:
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
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:NCVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:L/VA:N/SC:L/SI:L/SA:NThe 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)andrequestOrg(request, this.env)fromcreateArtifactUploadsintoartifactUploadResponse, then build keys using an unambiguous org+owner namespace such asconfigPrefix/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.tsauthenticates non-health, non-agent coordinator routes before fleet handling and writes trustedx-crabbox-ownerandx-crabbox-orgheaders from the auth context.worker/src/auth.tsverifies signed user tokens carrying bothownerandorg, andrequestWithAuthContextforwards both values to downstream fleet handlers.worker/src/fleet.ts:8879-8884handlesPOST /v1/artifacts/uploads, reads the authenticated JSON body, and callsartifactUploadResponse(this.env, input, requestOwner(request)); it does not passrequestOrg(request, this.env).worker/src/artifacts.ts:57-68accepts onlyowneras identity context and computesprefix = artifactPrefix(config.prefix, owner, request.prefix).worker/src/artifacts.ts:191-213builds 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-16075asserts the current brokered key shape asqa/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-253shows the CLI sends caller-selectedPrefixand fileNamevalues to the broker and trusts the returnedKeyandURLfor publication.internal/cli/coordinator.go:1644-1647sends artifact upload requests toPOST /v1/artifacts/uploads.Counterevidence considered:
internal/cli/artifacts_publish.go:255-273generates 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-180rejects 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:
worker/test/fleet.test.tscase that sends two authenticatedPOST /v1/artifacts/uploadsrequests with the same owner, prefix, and file name but differentx-crabbox-orgvalues, and assert the returnedprefix,key, upload URL, and read URL differ.worker/src/artifacts.tsunit 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