Skip to content

[security] GitHub OAuth can bind signed user tokens to an unverified public email #697

Description

@coygeek

Summary

Crabbox documents GitHub browser login as a supported coordinator authentication boundary: signed cbxu_ user tokens carry the caller's owner, org, and GitHub login, and normal user tokens are scoped to their own owner/org resources. That boundary depends on the owner claim being anchored to a verified GitHub identity.

At commit 0ec69d6427646c7bddffbac2c33576f211c203b1, githubIdentity in worker/src/oauth.ts prefers verified email addresses from /user/emails, but it initializes owner from user.email returned by /user and keeps that value whenever /user/emails returns no verified address. GitHub documents /user.email as the publicly visible profile email, while /user/emails is the endpoint that reports verified state. As a result, a successful GitHub OAuth login can produce a signed Crabbox user token whose trusted owner is a public profile value rather than a verified email.

This is in scope under Crabbox's security policy because it affects coordinator authentication, owner scoping, and optional admin-owner promotion for the supported trusted-team model. It does not depend on hostile tenants sharing one broker or on a trusted operator attacking their own leased machine.

Affected Components

  • worker/src/oauth.ts:197-201: the OAuth authorization URL requests read:user user:email read:org.
  • worker/src/oauth.ts:231-251: the callback exchanges the GitHub code, resolves identity.owner, checks allowed org/team membership by identity.login, and signs a Crabbox user token.
  • worker/src/oauth.ts:428-456: githubIdentity starts with owner = user.email || "", overwrites it only if /user/emails contains a verified address, and otherwise keeps the public email fallback.
  • worker/src/auth.ts:166-192: issueUserToken signs the selected owner into the cbxu_ token payload.
  • worker/src/auth.ts:90-101: authenticateRequest trusts the signed payload's owner, org, and login for GitHub-authenticated requests.
  • worker/src/auth.ts:107-117: githubUserIsAdmin promotes a signed user token to admin if payload.owner matches CRABBOX_GITHUB_ADMIN_OWNERS or payload.login matches CRABBOX_GITHUB_ADMIN_LOGINS.
  • worker/src/http.ts:29 and worker/src/fleet.ts: coordinator handlers use the trusted x-crabbox-owner/x-crabbox-org request context for lease, run, usage, bridge, portal, and sharing authorization decisions.

Attack Path

Attacker role: a GitHub user who passes the deployment's configured GitHub org/team allowlist and can complete crabbox login --url <broker-url>.

Prerequisites:

  • GitHub browser login is enabled.
  • The attacker is an active member of an allowed org/team.
  • The GitHub API response for /user/emails contains no verified email for the attacker, while /user.email contains a public profile email value.
  • For admin impact, the deployment also grants admin by CRABBOX_GITHUB_ADMIN_OWNERS and that owner list contains the public-email value selected by the attacker. CRABBOX_GITHUB_ADMIN_LOGINS is not affected by this email fallback because it compares the GitHub login, not owner.

Steps:

  1. The attacker configures their GitHub public profile email to the owner value they want Crabbox to trust.
  2. The attacker completes the normal GitHub OAuth flow through crabbox login --url <broker-url>.
  3. githubAuthCallback calls githubIdentity(accessToken).
  4. githubIdentity reads /user, sets owner from user.email, then calls /user/emails.
  5. If /user/emails has no verified: true entry, githubIdentity keeps the public profile email instead of failing closed or falling back to a non-email login-derived identity.
  6. issueUserToken signs that value into the cbxu_ token as payload.owner.
  7. Later requests authenticate as auth: "github" with owner: payload.owner; requestWithAuthContext injects it as x-crabbox-owner.
  8. Fleet, portal, usage, run, bridge, and sharing handlers use the trusted owner/org context for visibility and management checks. If the forged owner matches a real owner in the same org, the attacker can be treated as that owner. If the forged owner also matches CRABBOX_GITHUB_ADMIN_OWNERS, the same signed user token receives admin scope.

The closest missing control is in worker/src/oauth.ts:445-456: Crabbox does not require the owner email to come from a verified /user/emails entry before signing it into a long-lived user token.

Impact

The supported coordinator identity boundary can bind a Crabbox user token to an unverified GitHub public profile email. That breaks the documented expectation in docs/security.md and docs/features/auth-admin.md that GitHub user tokens are trusted owner/org credentials for the authenticated GitHub user.

Practical outcomes include:

  • Cross-owner impersonation inside the same allowed org if an attacker can make Crabbox sign an owner value that matches another user's Crabbox owner identity.
  • Unauthorized visibility or management of owner-scoped leases, runs, logs, usage, portal state, bridge attachments, and sharing state where handlers compare the trusted owner/org context.
  • Optional admin escalation when a deployment uses CRABBOX_GITHUB_ADMIN_OWNERS and the selected public email matches an admin-owner value.
  • Audit and attribution confusion because repeated logins can produce different owner identities for the same GitHub login when the public email fallback changes.

The issue is bounded by the GitHub org/team allowlist: the attacker must still be accepted by the configured GitHub membership checks. It is nevertheless a reportable Crabbox boundary issue because the flawed value is persisted in a signed coordinator credential and consumed by authorization code.

Severity Assessment

CVSS Assessment

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

The primary score models cross-owner confidentiality impact plus limited integrity and availability impact from owner-scoped management operations. Deployments that use CRABBOX_GITHUB_ADMIN_OWNERS with collidable owner values can have higher integrity and availability impact, but the base score avoids assuming that optional configuration.

Recommended Remediation

Require the signed owner to come from a verified GitHub email source, or use a non-email stable GitHub identity when no verified email is available.

Suggested changes:

  1. In githubIdentity, do not initialize or preserve owner from /user.email.
  2. Select owner only from /user/emails entries with verified: true, preferring the primary verified email.
  3. If no verified email exists, either reject the OAuth callback with GitHubAuthorizationError or deliberately use a stable login/id-derived owner such as <login>@users.noreply.github.com with documentation that it is not an email-verified owner.
  4. Keep admin-owner grants tied only to verified owner values. Continue using CRABBOX_GITHUB_ADMIN_LOGINS for login-based grants where that is the intended deployment policy.
  5. Add regression tests covering the no-verified-email case and the admin-owner promotion case.

Validation

Validation method: static source review against the isolated verification checkout plus official GitHub REST API documentation.

Evidence:

  • worker/src/oauth.ts:197-201 requests read:user user:email read:org, so Crabbox has the API scope needed to query verified email state.
  • worker/src/oauth.ts:439-456 fetches /user, sets owner to user.email, then only overwrites it when /user/emails contains a verified primary or verified secondary email.
  • worker/src/oauth.ts:231-251 passes identity.owner directly into issueUserToken.
  • worker/src/auth.ts:166-192 signs input.owner into the cbxu_ token payload without recording whether it came from a verified GitHub email.
  • worker/src/auth.ts:90-101 trusts the signed owner for every GitHub-token request.
  • worker/src/auth.ts:107-117 checks that same signed owner against CRABBOX_GITHUB_ADMIN_OWNERS.
  • docs/security.md describes signed user tokens as carrying owner, org, and GitHub login, and says admin scope can come from a signed GitHub user token whose verified email or login matches the configured admin lists.
  • docs/features/auth-admin.md documents that GitHub user tokens get their trusted owner/org/login from the signed token and that normal user tokens are scoped to their own resources.
  • GitHub's /user documentation identifies email as the publicly visible profile email, while /user/emails is the endpoint that returns email entries with a verified field.

Counterevidence and limits:

  • GitHub org/team membership is still checked by login before the token is issued.
  • CRABBOX_GITHUB_ADMIN_LOGINS is not affected by the email fallback because it compares the signed login.
  • Admin escalation through CRABBOX_GITHUB_ADMIN_OWNERS depends on deployment configuration. The owner-identity binding bug and cross-owner authorization risk do not require that optional admin list.
  • Runtime reproduction was not attempted because the issue is fully visible in the OAuth/auth source path and no source edits or external GitHub test account should be created for verification.

Suggested regression tests:

  • Mock /user to return { "login": "attacker", "email": "victim@example.test" } and /user/emails to return only unverified entries or an empty list. The OAuth callback should fail closed, or should issue a token with the documented non-email fallback rather than victim@example.test.
  • Mock a token issued from the no-verified-email path and assert that authenticateRequest does not promote it through CRABBOX_GITHUB_ADMIN_OWNERS.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P0Emergency: data loss, security bypass, crash loop, or unusable core runtime.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:auth-providerThis issue is about auth, provider routing, model choice, or SecretRef resolution.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