Skip to content

feat(miner-ui): require a same-origin session cookie on the local API#5605

Merged
JSONbored merged 2 commits into
mainfrom
miner-ui-local-api-auth
Jul 13, 2026
Merged

feat(miner-ui): require a same-origin session cookie on the local API#5605
JSONbored merged 2 commits into
mainfrom
miner-ui-local-api-auth

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Summary

  • Adds apps/gittensory-miner-ui/vite-auth.ts: a random token generated once per dev-server process, delivered to the browser as an HttpOnly; SameSite=Strict cookie on every response, and required on every /api/* request. An unauthenticated request gets 401.
  • Registered as the first plugin in vite.config.ts, ahead of runStateApiPlugin/portfolioQueueApiPlugin/ledgersApiPlugin — an unauthenticated /api/* request is rejected before any of their middlewares run, so any future /api/* endpoint (e.g. a write action) is covered automatically with no per-endpoint wiring.
  • No client-side fetch code changes needed: because the token rides on the browser's own cookie jar, a browser that has loaded this app's own page (/) already carries the cookie on every subsequent same-origin fetch() call.
  • Documents the mechanism in the app's README.md.

Why a cookie instead of a header/query token

The previous state was undocumented, implicit, and inconsistent: vite-run-state-api.ts had its own isLoopbackAddress check, but vite-portfolio-queue-api.ts and vite-ledgers-api.ts had no gate at all. Loopback-IP checking also isn't real authentication — it doesn't stop another local process, or a malicious page open in the same browser, from hitting the API.

HttpOnly keeps the token unreachable from any XSS in the SPA itself. SameSite=Strict means a cross-origin page — including a DNS-rebinding attempt, which resolves an attacker-controlled hostname to 127.0.0.1 rather than reusing this dev server's own origin — never has the cookie attached automatically by the browser. This satisfies the issue's literal acceptance criterion (an unauthenticated request is rejected) without touching the three existing read-only API files or any client fetcher.

Closes #4858

Test plan

  • npm --workspace @loopover/ui-miner run typecheck (via npm run ui:typecheck, which builds @loopover/ui-kit first)
  • npm --workspace @loopover/ui-miner run lint — 0 errors (5 pre-existing, unrelated warnings)
  • npm --workspace @loopover/ui-miner run test — 72/72 passing (15 new in src/auth.test.ts, covering cookie parsing edge cases, the 401/fall-through decision, and the actual Vite middleware wiring for both configureServer and configurePreviewServer)
  • npm --workspace @loopover/ui-miner run build
  • Manually started the real dev server and curled it end-to-end: an unauthenticated GET /api/portfolio-queue returns 401 with the documented error body; loading / captures the Set-Cookie; replaying that cookie on GET /api/portfolio-queue returns 200 with the real summary payload.

This change lives entirely under apps/gittensory-miner-ui/**, which vitest.config.ts's root coverage.include explicitly excludes (apps/**) — Codecov's patch gate does not apply here, matching the precedent set by #5479 (the PR this issue was blocked on).

Adds vite-auth.ts: a random per-process token delivered as an HttpOnly
SameSite=Strict cookie, required on every /api/* request. Replaces the
previous inconsistent/implicit loopback-IP trust (only one of the three
API plugins even checked the caller's address) with real authentication
that needs no client-side fetch changes.
@superagent-security

Copy link
Copy Markdown
Contributor

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

@loopover-orb loopover-orb Bot added the gittensor:feature Gittensor-scored feature linked to a feature issue — scores a 0.25x multiplier. label Jul 13, 2026
@loopover-orb

loopover-orb Bot commented Jul 13, 2026

Copy link
Copy Markdown

Caution

🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥🟥

🛑 Gittensory review result - fixes required

Review updated: 2026-07-13 09:13:54 UTC

4 files · 1 AI reviewer · no blockers · readiness 100/100 · CI failing · blocked

🛑 Suggested Action - Manual Review

Review summary
Adds a single Vite plugin that stamps an HttpOnly/SameSite=Strict session cookie on every response and gates all `/api/*` requests behind it, registered first in the plugin chain so it runs ahead of the three existing API plugins. The mechanism is sound: SameSite=Strict blocks the DNS-rebinding/cross-origin scenario described, HttpOnly blocks XSS exfiltration, and because the token rides the browser's own cookie jar no client fetch code needs to change. Test coverage is thorough (cookie parsing edge cases, 401 vs fall-through, both configureServer/configurePreviewServer hooks), and the README update accurately describes only what's implemented (dev/preview only).

Blockers

  • apps/gittensory-miner-ui/vite-auth.ts:83 sets the valid auth token on every response before rejecting unauthenticated `/api/*` requests, so any local process can request `/api/portfolio-queue`, read the `Set-Cookie` header from the 401, and replay the request authenticated; change it so `/api/*` rejects before issuing the session cookie, while non-API app loads still receive it.
Nits — 7 non-blocking
  • vite-auth.ts's isAuthenticatedRequest uses a plain `===` string comparison instead of a timing-safe compare (e.g. `crypto.timingSafeEqual`); low-severity for a localhost dev tool but worth noting given the file explicitly frames this as 'real' auth.
  • The external brief's 'leaked secret' flag on auth.test.ts:5 (`const TOKEN = "deterministic-test-token"`) is a false positive — it's a hardcoded test fixture, not a real credential.
  • vite-auth.ts:33 (`randomBytes(24)`) and :67 (`401`) are unexplained magic numbers; a named constant would self-document intent, though both are self-evident in context.
  • README correctly scopes the guarantee to `vite dev`/`vite preview`, but per the app's own README ('not a Wrangler deploy target... local miner CLI can serve later'), this cookie check is tied to Vite's plugin hooks and won't apply if the static build is ever served by a different process — worth a forward-looking note if that CLI-serve path is imminent.
  • Consider `crypto.timingSafeEqual` for the token comparison in isAuthenticatedRequest (vite-auth.ts:51) since this is presented as a genuine security boundary, not just a dev convenience.
  • PR author also opened the linked issue — Link an issue that was opened by a different contributor, or provide a rationale for why this self-authored issue represents genuine discovery work.
  • Possible secret-shaped assignment in the diff (generic_secret_assignment) — Verify the value is not a real credential.

CI checks failing

  • validate
  • validate-code
Signal Result Evidence
Code review ✅ No blockers 1 reviewer
Linked issue ✅ Linked #4858
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 (1 linked issue).
Validation posture ✅ 25/25 PR body includes validation/test evidence.
Contributor workload ✅ 10/10 Author activity: 44 registered-repo PR(s), 36 merged, 347 issue(s).
Contributor context ✅ Confirmed Gittensor contributor JSONbored; Gittensor profile; 44 PR(s), 347 issue(s).
Gate result ✅ Passing No configured blocker found.
Improvement ✅ Minor risk: clean · value: minor — Code changes are accompanied by test evidence.
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: Python, TypeScript, Ruby, Go, JavaScript, MDX, Shell, Solidity
  • Official Gittensor activity: 44 PR(s), 347 issue(s).
  • PR-specific overlap: none found.
Contributor next steps
  • Treat this as maintainer-lane context rather than normal contributor-lane activity.
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.
[BETA] Chat with Gittensory

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

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

Full command reference: https://gittensory.aethereal.dev/docs/gittensory-commands

Visual preview
Route Viewport Before (production) After (this PR's preview) Diff
/ desktop before / after /
/ mobile before / (mobile) after / (mobile)

Click any thumbnail to open the full-size screenshot. Before = production · After = this PR's preview deploy.

🟩 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 Gittensory, a quiet PR intelligence layer for OSS maintainers.

  • Re-run Gittensory review

@loopover-orb loopover-orb Bot added the manual-review Gittensor contributor context label Jul 13, 2026
Set-Cookie was applied unconditionally before the auth check, so an
unauthenticated /api/* request could read the valid token straight off
its own 401 response and replay it -- defeating the whole mechanism.
Reject first, then only stamp the cookie on a response the caller is
already authorized to receive (an authenticated /api/* request, or any
non-/api/* page/asset load).
@JSONbored

Copy link
Copy Markdown
Owner Author

Fixed in 01c0ad5. Set-Cookie was applied unconditionally before the auth check ran, so an unauthenticated /api/* request could read the valid token straight off its own 401 and replay it — you're right, that defeats the mechanism entirely. Reordered so the auth check runs first: a rejection short-circuits before any setHeader("Set-Cookie", ...) call, and the cookie is only stamped on a response the caller is already authorized to receive (an authenticated /api/* request, or any non-/api/* page/asset load).

Added a regression test asserting Set-Cookie is absent from the 401 response, plus verified live: curled /api/portfolio-queue unauthenticated and confirmed no Set-Cookie header on the 401, then confirmed the legitimate page-load → cookie → authenticated-request flow still works end-to-end.

The ui:version-audit CI failure (MCP npm dist-tag drift) looks unrelated to this diff — #5608 (opened around the same time, untouched by this bug) already shows that check passing on a later run, so it looks like transient/external drift that should clear on this push's fresh CI run.

@JSONbored JSONbored merged commit 0e48728 into main Jul 13, 2026
8 of 9 checks passed
@JSONbored JSONbored deleted the miner-ui-local-api-auth branch July 13, 2026 09:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gittensor:feature Gittensor-scored feature linked to a feature issue — scores a 0.25x multiplier. manual-review Gittensor contributor context

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add auth to the local miner-ui API

1 participant