fix(security): require auth on GET /api/orders and GET /api/admin/metrics #11

Merged
admin merged 1 commits from security/auth-gate-anonymous-api-routes into main 2026-08-11 09:35:48 +00:00
Owner

Summary

A live compliance audit against https://0pt.app confirmed these routes responded HTTP 200 with zero authentication:

  • GET /api/orders -> full order queue
  • GET /api/admin/metrics -> financial pipeline aggregates (deposit/pipeline totals)

middleware.ts's matcher only covers /api/0pt/:path* by design (see its own header comment: "does NOT touch ... the unrelated payment/order APIs"), so nothing upstream of these two handlers ever verified the caller.

Fix

Both GET handlers now call the same fail-closed Zitadel verifier /api/0pt/* already relies on (resolveAuthContext, src/auth/context.ts), via a new thin wrapper src/auth/require-route-auth.ts that maps a rejected decision to the identical 401/403 JSON shape middleware.ts returns.

The wrapper re-verifies the bearer token itself rather than trusting any x-0pt-verified-* header on the incoming request -- middleware never runs in front of these routes to overwrite a client-supplied one, so trusting it would be a trivial bypass.

POST /api/orders (anonymous draft/quote creation before the deposit-payment step) is left as-is intentionally -- only the two read paths that leaked data are gated, per the audit.

Files changed

  • app/api/orders/route.ts -- GET now calls requireRouteAuth
  • app/api/admin/metrics/route.ts -- GET now calls requireRouteAuth
  • src/auth/require-route-auth.ts (new) -- thin Next.js wrapper around resolveAuthContext
  • tests/api/anonymous-routes-auth.test.ts (new) -- 6 regression tests, incl. spoofed-header and dev-escape-hatch cases

Verification

  • pnpm run typecheck (blocking CI gate): clean, 0 errors
  • pnpm run test (blocking CI gate): 134 passed, 1 pre-existing skip (unrelated live-infra test), 0 failed -- includes the 6 new tests
  • pnpm run build (blocking CI gate): succeeds, /api/orders and /api/admin/metrics both compile as dynamic (ƒ) routes
  • pnpm run lint (advisory, non-blocking per .gitea/workflows/ci.yml): pre-existing errors only, none in the files this PR touches

Context / known adjacent issues (not fixed here, out of scope)

  • docs/compliance/BACKLOG.md already tracks this as P0-3 ("the whole non-/api/0pt API lacks auth": orders/payments/admin/subscriptions/uploads/estimates/screen/ai). This PR is a scoped, partial fix -- payments/subscriptions/uploads/estimates/screen/ai remain open.
  • The same backlog also tracks P0-1: middleware reportedly does not run at all in the live Vercel production build (root cause not yet found), which means even the already-gated /api/0pt/* routes were observed answering anonymously live. This PR fixes the code-level gap in this repo; it does not change what's actually deployed at 0pt.app today.
  • app/api/orders/[id]/route.ts (GET + PATCH status transition) has the same missing-auth pattern and was not touched here to keep this diff surgical -- flagging as a likely follow-up.

Does not touch docs/compliance/*, services/scrapers/*, or the evasion-stack files (separate, already-escalated decision).

## Summary A live compliance audit against `https://0pt.app` confirmed these routes responded `HTTP 200` with **zero authentication**: - `GET /api/orders` -> full order queue - `GET /api/admin/metrics` -> financial pipeline aggregates (deposit/pipeline totals) `middleware.ts`'s matcher only covers `/api/0pt/:path*` by design (see its own header comment: "does NOT touch ... the unrelated payment/order APIs"), so nothing upstream of these two handlers ever verified the caller. ## Fix Both `GET` handlers now call the **same fail-closed Zitadel verifier** `/api/0pt/*` already relies on (`resolveAuthContext`, `src/auth/context.ts`), via a new thin wrapper `src/auth/require-route-auth.ts` that maps a rejected decision to the identical 401/403 JSON shape `middleware.ts` returns. The wrapper **re-verifies the bearer token itself** rather than trusting any `x-0pt-verified-*` header on the incoming request -- middleware never runs in front of these routes to overwrite a client-supplied one, so trusting it would be a trivial bypass. `POST /api/orders` (anonymous draft/quote creation before the deposit-payment step) is left as-is intentionally -- only the two read paths that leaked data are gated, per the audit. ## Files changed - `app/api/orders/route.ts` -- `GET` now calls `requireRouteAuth` - `app/api/admin/metrics/route.ts` -- `GET` now calls `requireRouteAuth` - `src/auth/require-route-auth.ts` (new) -- thin Next.js wrapper around `resolveAuthContext` - `tests/api/anonymous-routes-auth.test.ts` (new) -- 6 regression tests, incl. spoofed-header and dev-escape-hatch cases ## Verification - `pnpm run typecheck` (blocking CI gate): clean, 0 errors - `pnpm run test` (blocking CI gate): 134 passed, 1 pre-existing skip (unrelated live-infra test), 0 failed -- includes the 6 new tests - `pnpm run build` (blocking CI gate): succeeds, `/api/orders` and `/api/admin/metrics` both compile as dynamic (ƒ) routes - `pnpm run lint` (advisory, non-blocking per `.gitea/workflows/ci.yml`): pre-existing errors only, none in the files this PR touches ## Context / known adjacent issues (not fixed here, out of scope) - `docs/compliance/BACKLOG.md` already tracks this as **P0-3** ("the whole non-`/api/0pt` API lacks auth": orders/payments/admin/subscriptions/uploads/estimates/screen/ai). This PR is a scoped, partial fix -- payments/subscriptions/uploads/estimates/screen/ai remain open. - The same backlog also tracks **P0-1**: middleware reportedly does not run at all in the live Vercel production build (root cause not yet found), which means even the already-gated `/api/0pt/*` routes were observed answering anonymously live. This PR fixes the code-level gap in this repo; it does not change what's actually deployed at `0pt.app` today. - `app/api/orders/[id]/route.ts` (`GET` + `PATCH` status transition) has the same missing-auth pattern and was **not** touched here to keep this diff surgical -- flagging as a likely follow-up. Does not touch `docs/compliance/*`, `services/scrapers/*`, or the evasion-stack files (separate, already-escalated decision).
admin added 1 commit 2026-08-11 02:08:29 +00:00
fix(security): require auth on GET /api/orders and GET /api/admin/metrics
Security Scan (SIAX Cloud) / security (pull_request) Successful in 24s
CI (SIAX Cloud) / security (pull_request) Successful in 25s
CI (SIAX Cloud) / quality (pull_request) Successful in 40s
CI (SIAX Cloud) / security (push) Successful in 22s
Security Scan (SIAX Cloud) / security (push) Successful in 20s
CI (SIAX Cloud) / quality (push) Successful in 39s
15a0dbe3b1
A live compliance audit against 0pt.app confirmed both endpoints returned
HTTP 200 with zero authentication -- the full order queue and financial
pipeline aggregates (deposit/pipeline totals) were readable by anyone.

Root cause: middleware.ts's matcher only covers /api/0pt/:path*, by design
("does NOT touch ... the unrelated payment/order APIs"), so nothing upstream
of these two handlers ever verified the caller.

Fix: both GET handlers now call the same fail-closed Zitadel verifier
/api/0pt/* relies on (resolveAuthContext, src/auth/context.ts) directly, via
a new thin wrapper (src/auth/require-route-auth.ts) that maps a rejected
decision to the same 401/403 JSON shape middleware.ts already returns. The
wrapper re-verifies the bearer token itself rather than trusting any
x-0pt-verified-* header on the request, since middleware never runs in front
of these routes to overwrite a client-supplied one.

POST /api/orders (anonymous draft/quote creation before the deposit-payment
step) is intentionally left as-is -- only the read paths that leaked data are
gated. Tracked as part of docs/compliance/BACKLOG.md P0-3.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
admin merged commit into main 2026-08-11 09:35:48 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: admin/0pt#11