Skip to content

fix(api): CORS credential leak + empty-scopes privilege escalation#376

Merged
duyet merged 2 commits into
mainfrom
claude/w7-api-security
Jul 17, 2026
Merged

fix(api): CORS credential leak + empty-scopes privilege escalation#376
duyet merged 2 commits into
mainfrom
claude/w7-api-security

Conversation

@duyet

@duyet duyet commented Jul 17, 2026

Copy link
Copy Markdown
Owner

Summary

Two security fixes, each in its own commit:

#345 — CORS reflects credentials for localhost origins in production, invalid wildcard-with-credentials

  • ALLOWED_ORIGINS hardcoded localhost/127.0.0.1 entries that were also served in production (single Worker, no per-env origin list). Any page open on those loopback ports could make credentialed cross-origin calls to the production API.
  • The handler answered Access-Control-Allow-Origin: * for a missing Origin, which is invalid alongside Access-Control-Allow-Credentials: true and rejected by browsers.
  • For a disallowed origin it reflected ALLOWED_ORIGINS[0] (https://agentstate.app) instead of omitting the header — misleading, since the response advertised an allow-origin that didn't match the request.
  • Fix: resolveAllowedOrigin() (packages/api/src/lib/cors.ts) returns null (no header) for a missing/disallowed origin, and only allows localhost origins when the Worker's ENVIRONMENT binding isn't "production". ENVIRONMENT=production is stamped only into the generated deploy config (prepare-wrangler-deploy-config.sh), so local wrangler dev and tests are unaffected.

#346 — Empty scopes array mints a full-access API key (privilege-escalation landmine)

  • buildApiKey stored scopes: scopes && scopes.length > 0 ? JSON.stringify(scopes) : null. An explicit empty array ([]) collapsed to null, and effectiveKeyScopes(null) resolves to full access (*) — the opposite of "no permissions".
  • services/keys.ts's response shaping had the identical bug.
  • The delegation guards in routes/keys.ts and routes/v1-keys.ts used if (childScopes); changed to if (childScopes !== undefined) to make the undefined-vs-explicit-[] distinction explicit at the call site (functionally a no-op today since arrays are always truthy, but removes the ambiguity flagged by the audit).
  • Only scopes === undefined (the field omitted entirely) now maps to null/full access. Legacy/unscoped keys (column null/absent) are unaffected and keep full access.
  • Not currently reachable over HTTP (CreateApiKeySchema.scopes has .min(1), unchanged), but the lib/service layer is the security-relevant unit — any future caller (internal job, new route, MCP tool) that reaches createApiKey/buildApiKey with [] now gets a zero-permission key instead of a god key.

Closes #345
Closes #346

Test plan

  • New regression tests in packages/api/test/cors.test.ts (unit tests for resolveAllowedOrigin covering missing/disallowed/production/dev-localhost cases, plus live SELF.fetch assertions that disallowed/missing origins get no Access-Control-Allow-Origin header) — all fail against the pre-fix code.
  • New regression tests in packages/api/test/scopes.test.ts (buildApiKey/createApiKey with undefined vs. explicit [] scopes) — fail against the pre-fix code (previously resolved to null/full access).
  • bunx biome check packages/api/src/ — clean
  • bunx tsc --noEmit -p packages/api/tsconfig.json — clean
  • cd packages/api && bunx vitest run — 366/366 passed (26 test files)

duyet and others added 2 commits July 17, 2026 08:58
…ials in CORS

ALLOWED_ORIGINS hardcoded localhost/127.0.0.1 entries that were also served in
production (single Worker, no per-env origin list), letting any page open on
those loopback ports make credentialed cross-origin calls to the production
API. The handler also answered "*" for a missing Origin and reflected
ALLOWED_ORIGINS[0] for a disallowed Origin — both invalid/misleading when
combined with Access-Control-Allow-Credentials: true.

resolveAllowedOrigin() (lib/cors.ts) now only allows localhost origins when
the Worker's ENVIRONMENT binding is not "production", and returns null (no
header) for a missing or disallowed origin instead of "*" or a fallback
origin. ENVIRONMENT=production is stamped only into the generated deploy
config (prepare-wrangler-deploy-config.sh), so local `wrangler dev` and tests
are unaffected.

Closes #345

Co-Authored-By: Duyet Le <me@duyet.net>
Co-Authored-By: duyetbot <bot@duyet.net>
buildApiKey collapsed scopes: [] to null in the DB column, and
effectiveKeyScopes(null) resolves to full access ("*") — the opposite of "no
permissions". createApiKey's response shaping had the same collapse. The
delegation guards in routes/keys.ts and routes/v1-keys.ts also used a truthy
check (if (childScopes)), which is fine for `[]` itself (arrays are always
truthy) but didn't make the undefined-vs-empty-array distinction explicit at
the call site.

Only `scopes === undefined` (omitted entirely) now maps to null/full access;
an explicit [] is persisted and reported as an empty scope set. This is not
currently reachable over HTTP (CreateApiKeySchema.scopes has .min(1)), but the
lib/service layer is the security-relevant unit per the audit — any future
caller (internal job, new route, MCP tool) that reaches createApiKey/
buildApiKey with [] must get a zero-permission key, not a god key. Legacy/
unscoped keys (scopes column null or absent) keep full access unchanged.

Closes #346

Co-Authored-By: Duyet Le <me@duyet.net>
Co-Authored-By: duyetbot <bot@duyet.net>
@gemini-code-assist

Copy link
Copy Markdown

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @duyet, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@duyet, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 43 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3765e20c-6f30-4cdf-a8f1-8691c8a6c84d

📥 Commits

Reviewing files that changed from the base of the PR and between aaf05ac and 9b9240b.

📒 Files selected for processing (10)
  • packages/api/scripts/prepare-wrangler-deploy-config.sh
  • packages/api/src/index.ts
  • packages/api/src/lib/api-key.ts
  • packages/api/src/lib/cors.ts
  • packages/api/src/routes/keys.ts
  • packages/api/src/routes/v1-keys.ts
  • packages/api/src/services/keys.ts
  • packages/api/src/types.ts
  • packages/api/test/cors.test.ts
  • packages/api/test/scopes.test.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/w7-api-security

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@duyet
duyet merged commit a6a0650 into main Jul 17, 2026
6 checks passed
@duyet
duyet deleted the claude/w7-api-security branch July 17, 2026 03:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant