Skip to content

[security] fix(auth): reject admin claims in user tokens#64

Merged
steipete merged 3 commits into
openclaw:mainfrom
Hinotoi-agent:fix/reject-admin-user-token-claims
May 9, 2026
Merged

[security] fix(auth): reject admin claims in user tokens#64
steipete merged 3 commits into
openclaw:mainfrom
Hinotoi-agent:fix/reject-admin-user-token-claims

Conversation

@Hinotoi-agent
Copy link
Copy Markdown
Contributor

Summary

This PR hardens the coordinator user-token trust boundary so signed GitHub/browser-login user tokens cannot carry or activate admin privileges.

  • Rejects signed cbxu_ user tokens that contain an admin claim.
  • Keeps admin access exclusively on the separate admin bearer token path.
  • Adds a regression test that signs an attacker-controlled user-token payload with the shared-token fallback key and confirms it is rejected.

Security issues covered

Issue Impact Severity
Signed user tokens could carry admin: true A holder of the shared non-admin token could forge a signed user token with admin privileges when CRABBOX_SESSION_SECRET falls back to CRABBOX_SHARED_TOKEN High

Before this PR

  • issueUserToken() did not issue admin user tokens, but verifyUserToken() accepted arbitrary signed payloads that included admin: true.
  • authenticateRequest() then mapped payload.admin === true into x-crabbox-admin: true for downstream routes.
  • Deployments without a distinct CRABBOX_SESSION_SECRET use CRABBOX_SHARED_TOKEN as the user-token HMAC key, so a shared-token holder could sign their own cbxu_ payload.
  • Existing tests covered that normal issued user tokens were non-admin, but not that forged signed user-token admin claims are rejected.

After this PR

  • User-token payloads are treated as non-admin only.
  • Any signed user token containing an admin claim is rejected during token verification.
  • The UserTokenPayload schema no longer includes admin.
  • A regression test verifies that a manually signed admin: true user token does not authenticate.

Why this matters

Crabbox separates normal automation/user access from coordinator admin access. Admin routes can view or manage global coordinator state, including all leases, pool state, image operations, and forced release/delete flows.

If signed user tokens can self-assert admin status, that separation can collapse in deployments where the user-token signing secret falls back to the shared non-admin token. A caller who should only have shared-token automation access could mint a cbxu_ token that passes the admin route gate.

How this differs from prior auth hardening

Previous public auth hardening focused on identity/header trust, such as ensuring caller-supplied Access identity headers do not override signed GitHub user-token identity.

This PR addresses a different boundary:

  • prior hardening: which owner/org identity is trusted for a signed user token;
  • this hardening: whether a signed user-token payload is allowed to contain an admin privilege bit at all.

Both boundaries matter. A user token can have the correct owner/org identity and still be unsafe if it can self-assert admin: true.

Attack flow

Holder of CRABBOX_SHARED_TOKEN
    -> signs a cbxu_ user-token payload with admin: true
        -> authenticateRequest() accepts the signed payload
            -> x-crabbox-admin becomes true
                -> admin-only coordinator routes are reachable

Affected code

Issue Files
Signed user-token admin claim acceptance worker/src/auth.ts, worker/test/http.test.ts

Root cause

Signed user-token admin claim acceptance:

  • verifyUserToken() validated signature, expiry, and identity fields, but did not reject an admin claim.
  • authenticateRequest() treated payload.admin === true as an admin authentication context even though GitHub/browser-login user tokens are documented as non-admin.
  • sessionSecret() can fall back to CRABBOX_SHARED_TOKEN, making this unsafe when no distinct CRABBOX_SESSION_SECRET is configured.

CVSS assessment

Issue CVSS v3.1 Vector
Signed user-token admin claim acceptance 8.8 High CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

Rationale:

  • The attacker needs a valid shared-token-level credential, so privileges required are low rather than none.
  • Exploitation is network-reachable and does not require user interaction.
  • Successful exploitation can grant coordinator admin privileges, affecting confidentiality, integrity, and availability of coordinator-managed resources.

Safe reproduction steps

  1. Configure auth with CRABBOX_SHARED_TOKEN and no distinct CRABBOX_SESSION_SECRET.
  2. Create a cbxu_ token payload containing:
    • typ: "crabbox-user"
    • normal owner/org/login fields
    • future exp
    • admin: true
  3. Sign the payload with the shared token using the same HMAC-SHA256 user-token format.
  4. Send it as Authorization: Bearer <forged-cbxu-token> to an admin-only route such as /v1/admin/leases.
  5. On vulnerable code, authentication produces an admin context. With this PR, authenticateRequest() rejects the token.

The regression test added in this PR performs the safe local version of this flow without contacting any real coordinator.

Expected vulnerable behavior

  • A manually signed user token with admin: true authenticates as auth: "github" and admin: true when the signer knows the user-token HMAC key.
  • If that HMAC key falls back to CRABBOX_SHARED_TOKEN, the non-admin shared token becomes enough to mint an admin user token.

Changes in this PR

  • Removes admin from the signed user-token payload type.
  • Always maps verified signed user tokens to admin: false.
  • Rejects signed user-token payloads that contain an admin claim.
  • Adds a regression test that signs an admin: true user-token payload and expects authentication to fail.

Files changed

Category Files What changed
Auth hardening worker/src/auth.ts Rejects admin-bearing user tokens and keeps signed user-token auth non-admin
Regression tests worker/test/http.test.ts Adds a forged signed-token test for the admin-claim path

Maintainer impact

  • Normal GitHub/browser-login user tokens continue to work.
  • Shared bearer-token automation remains unchanged.
  • Admin automation continues to use CRABBOX_ADMIN_TOKEN through the existing admin bearer-token path.
  • The patch is intentionally narrow and does not change lease, run, portal, or provider behavior.

Fix rationale

GitHub/browser-login user tokens are documented as non-admin. The most durable boundary is therefore to make the user-token verifier reject admin-bearing payloads and make the authenticated context non-admin by construction.

This avoids depending on every deployment having a distinct session secret before the admin boundary is safe, while still preserving the existing recommendation to keep CRABBOX_SESSION_SECRET as a separate Worker secret.

Type of change

  • Security fix
  • Tests
  • Documentation update
  • Refactor with no behavior change

Test plan

  • Focused coordinator auth regression test
  • Full Worker Vitest suite
  • Worker type check
  • Worker lint
  • Git whitespace check

Executed with:

  • npm test --prefix worker -- http.test.ts
  • npm test --prefix worker
  • npm run check --prefix worker
  • npm run lint --prefix worker
  • git diff --check

Token usage

  • discovery tokens: partial/unknown
  • validation tokens: partial/unknown
  • duplicate-check tokens: partial/unknown
  • PR/writeup tokens: partial/unknown
  • total tokens: partial/unknown
  • notes: Exact aggregate token telemetry was not available for the whole multi-step audit and PR preparation. Duplicate checks included exact GitHub searches for admin/cbxu_, admin claims/signed user, CRABBOX_SESSION_SECRET/CRABBOX_SHARED_TOKEN, payload.admin, shared token/admin, and repo security advisories.

Disclosure notes

  • This PR is bounded to signed user-token admin-claim handling.
  • It does not change the documented shared-token owner/org automation model.
  • It does not claim a bypass of Cloudflare Access, GitHub org checks, or the separate CRABBOX_ADMIN_TOKEN path.
  • Public duplicate checks did not find an existing issue, PR, or advisory for this exact shared-token-to-admin user-token escalation.

@steipete steipete force-pushed the fix/reject-admin-user-token-claims branch from 1e4d933 to 05bae43 Compare May 9, 2026 22:36
@steipete steipete merged commit 46079f6 into openclaw:main May 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants