Skip to content

fix(api): open the public no-credential routes to any CORS origin - #6401

Merged
JSONbored merged 1 commit into
mainfrom
fix/cors-public-no-credential-routes
Jul 16, 2026
Merged

fix(api): open the public no-credential routes to any CORS origin#6401
JSONbored merged 1 commit into
mainfrom
fix/cors-public-no-credential-routes

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Summary

  • `/health`, `/v1/public/stats`, and `/v1/public/github/repos/:owner/:repo/stats` are unauthenticated, cookie-free, aggregate-only endpoints, but sat behind the same strict exact-match CORS allowlist as every authenticated route.
  • Confirmed live via Loki: browserless's visual-review capture of PR previews was hitting real CORS errors calling these from a fresh `-loopover-ui..workers.dev` preview build. Cloudflare assigns a random hostname per deploy (`ui-preview-deploy.yml`), so a static exact-match allowlist can never enumerate them by design.
  • Deliberately scoped narrow. I initially considered just widening the CORS origin allowlist to any `.workers.dev`/`.pages.dev` origin (there's precedent for trusting that suffix elsewhere in this codebase, `src/review/visual/shot.ts`'s SSRF-guarded screenshot host allowlist) -- but this app's global CORS middleware also sets `Access-Control-Allow-Credentials: true`, and this app has real HttpOnly session cookies. Widening the credentialed allowlist itself would let any third party hosted on that same shared platform ride an authenticated user's session cross-origin. That's a real security regression I caught before shipping, not a hypothetical.
  • Instead this adds a separate, credential-free CORS branch for exactly these 3 known-public routes (mirrors the existing `handleStats` `"*"`-with-no-credentials pattern already used for a comparable public feed), gated by a new `isPublicNoCredentialRoute()` path check. Every other route's CORS behavior -- the strict allowlist + credentials -- is completely unchanged.

Test plan

  • New `test/unit/routes-cors.test.ts`: confirms the 3 public routes (including a GET with dynamic path segments, and an OPTIONS preflight) open to an arbitrary `.workers.dev`/`.pages.dev` origin with no credentials header
  • REGRESSION: an authenticated route from the same unlisted preview origin gets NO CORS headers at all (still protected)
  • REGRESSION: a genuinely allowlisted origin on a non-public route still gets the credentialed treatment, unchanged
  • Confirms a public-but-not-in-the-3-route-list endpoint (`/v1/public/subnet-interface`) correctly does NOT get the open treatment -- only the routes that were actually failing
  • Updated the 2 existing `test/integration/api.test.ts` assertions that expected the old reflected-origin behavior for `/health` and the GitHub-repo-stats route
  • Full `npm run test:ci` gate green

/health, /v1/public/stats, and /v1/public/github/repos/:owner/:repo/stats
are unauthenticated, cookie-free, aggregate-only endpoints, but sat
behind the same strict exact-match CORS allowlist as every authenticated
route. Confirmed live: browserless's visual-review capture of PR
previews was hitting real CORS errors calling these from a fresh
<alias>-loopover-ui.<sub>.workers.dev preview build -- Cloudflare
assigns a random hostname per deploy (ui-preview-deploy.yml), so a
static allowlist can never enumerate them.

Adds a separate, credential-free CORS branch (mirrors the existing
handleStats "*" pattern) for exactly these 3 routes, gated by a new
isPublicNoCredentialRoute() path check. Deliberately does NOT touch the
global allowedCorsOrigin()/Access-Control-Allow-Credentials path for
anything else -- this app has real HttpOnly session cookies, so
widening the credentialed-CORS allowlist itself to any *.workers.dev/
*.pages.dev origin would let any third party hosted on that same shared
platform ride an authenticated user's session cross-origin. Every other
route's CORS behavior is unchanged (see the REGRESSION tests in
routes-cors.test.ts confirming this).
@superagent-security

Copy link
Copy Markdown
Contributor

Superagent didn't find any vulnerabilities or security issues in this PR.

@codecov

codecov Bot commented Jul 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.60%. Comparing base (7ee0670) to head (7f84431).
⚠️ Report is 29 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #6401   +/-   ##
=======================================
  Coverage   95.60%   95.60%           
=======================================
  Files         601      601           
  Lines       47310    47320   +10     
  Branches    15050    15054    +4     
=======================================
+ Hits        45232    45242   +10     
  Misses       1291     1291           
  Partials      787      787           
Flag Coverage Δ
shard-1 43.93% <16.66%> (-0.10%) ⬇️
shard-2 36.93% <100.00%> (+0.28%) ⬆️
shard-3 32.29% <61.11%> (-0.24%) ⬇️
shard-4 34.65% <11.11%> (+0.07%) ⬆️
shard-5 31.43% <11.11%> (-0.16%) ⬇️
shard-6 45.42% <94.44%> (+0.45%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/api/routes.ts 94.50% <100.00%> (+0.02%) ⬆️

@loopover-orb loopover-orb Bot added the gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier. label Jul 16, 2026
@loopover-orb

loopover-orb Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Warning

⏸️ LoopOver review result - manual review recommended

Review updated: 2026-07-16 07:03:55 UTC

3 files · 1 AI reviewer · 2 blockers · CI green · clean

⏸️ Suggested Action - Manual Review

Review summary
This PR splits the global CORS middleware into two branches: a wildcard, credential-free branch for three known-public, unauthenticated GET routes (health, public stats, per-repo stats), and the existing strict allowlist+credentials branch for everything else. The isPublicNoCredentialRoute check is a tight allowlist of exact/regex path matches, credentials are never set in the new branch, and the accompanying tests cover the 3 public routes, an OPTIONS preflight, and two regression cases confirming non-public and authenticated routes are unaffected. The security reasoning (avoiding widening the credentialed allowlist to a shared-hostname suffix) is sound and the change is narrowly scoped to the stated problem.

Nits — 5 non-blocking
  • The new branch drops `Access-Control-Allow-Headers: mcp-session-id, mcp-protocol-version` and narrows `Access-Control-Allow-Methods` to `GET, OPTIONS` (src/api/routes.ts ~961-963); confirm none of the 3 public routes are ever called with MCP headers or non-GET methods, since a future MCP client hitting these paths cross-origin would silently fail preflight.
  • The new branch omits `Vary: Origin` (src/api/routes.ts ~964) even though the response is now origin-independent (`*`), which is correct, but worth a one-line comment noting that's intentional so a future editor doesn't 're-add' it by copying the else-branch pattern.
  • `isPublicNoCredentialRoute` (src/api/routes.ts ~5975) is a hard-coded path list; if a 4th public route is added later it's easy to forget updating this function — consider colocating it with the route registration or referencing the route table directly.
  • Consider extracting the shared header-setting logic (Allow-Methods, Max-Age) between the two branches to reduce duplication, though the current explicit duplication is arguably clearer for a security-sensitive branch.
  • Add a code comment cross-reference from `requiresApiToken` to `isPublicNoCredentialRoute` (src/api/routes.ts) since both are path-classification functions for public vs protected routes and could drift out of sync.

Concerns raised — review before merging

  • No linked issue detected — If this PR is intended to solve an issue, link it explicitly in the PR body.
  • Maintainer requires a linked issue — Link the relevant issue (for example Closes #123) before opening the PR.
📋 Copy for AI agents — paste into your coding agent
Fix the following blocker(s) from this PR review:

1. No linked issue detected — If this PR is intended to solve an issue, link it explicitly in the PR body.

2. Maintainer requires a linked issue — Link the relevant issue (for example `Closes #123`) before opening the PR.

Decision drivers

  • ❌ Code review — 2 blockers (1 reviewer)
  • ❌ Gate result — Blocking (Repo-configured hard blocker found.)
Context & advisory signals — never blocks the verdict
Signal Result Evidence
Linked issue ⚠️ Missing No linked issue or no-issue rationale found.
Related work ✅ No active overlap found No same-issue or scoped active PR overlap found.
Change scope ✅ 20/20 Low review scope from cached public metadata (no linked issue context).
Validation posture ✅ 25/25 PR body includes validation/test evidence.
Contributor workload ✅ 10/10 Author activity: 42 registered-repo PR(s), 34 merged, 374 issue(s).
Contributor context ✅ Confirmed Gittensor contributor JSONbored; Gittensor profile; 42 PR(s), 374 issue(s).
Improvement ✅ Minor risk: clean · value: minor
Review context
  • Author: JSONbored
  • Role context: owner (maintainer lane)
  • Public audience mode: oss maintainer
  • Lane context: Repository is configured for direct PR review.
  • Public profile languages: not available
  • Official Gittensor activity: 42 PR(s), 374 issue(s).
  • PR-specific overlap: none found.
Contributor next steps
  • Start here: Treat this as maintainer-lane context rather than normal contributor-lane activity.
  • Then work through the remaining 2 steps in the Signals table above.
Signal definitions
  • Related work = same linked issue, overlapping active PRs, or title/path similarity.
  • Change scope = cached public metadata such as size labels, draft state, and review-burden hints.
  • Validation posture = whether the PR provides enough public validation/test evidence for maintainer review.
  • Contributor workload = public contributor activity and cleanup pressure, not a repo-wide quality failure.
  • Contributor context = public GitHub/Gittensor identity context; non-Gittensor status is not a blocker.
🧪 Chat with LoopOver

Ask LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.

  • @loopover ask &lt;question&gt; answers contribution-quality Q&A with source citations and freshness.
  • @loopover chat &lt;question&gt; answers in natural prose from cached decision-pack facts via local inference (maintainer/collaborator; read-only).
  • A plain-language @loopover mention with a real question is routed to the closest matching read-only command automatically — no exact syntax required.

Full command reference: https://loopover.ai/docs/loopover-commands

🧪 Experimental — new and may change.

🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed


💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →.

Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.

  • Re-run LoopOver review

@JSONbored JSONbored self-assigned this Jul 16, 2026
@loopover-orb loopover-orb Bot added the manual-review Gittensor contributor context label Jul 16, 2026
@JSONbored
JSONbored merged commit de20c8e into main Jul 16, 2026
17 checks passed
@JSONbored
JSONbored deleted the fix/cors-public-no-credential-routes branch July 16, 2026 07:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier. manual-review Gittensor contributor context

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant