mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-05-27 09:32:28 +02:00
bcc2d036b3
External audit by @tg12 found three coupled vulnerabilities in the
Next.js admin-auth surface that together let any webpage the operator
visits trigger arbitrary privileged backend calls:
#249/#254 — Cross-origin webpages can have process.env.ADMIN_KEY
injected into their forwarded backend requests just by
issuing fetch('http://localhost:3000/api/wormhole/...')
from a browser tab the operator has open. Full
identity-takeover CSRF.
#255 — When ADMIN_KEY is unset on the server (the default in
.env.example), the admin session route fell through to
GET /api/settings/privacy-profile to "verify" the user-
supplied key. That endpoint is public; it always returns
200 for any X-Admin-Key value. So arbitrary attacker
keys minted full admin session cookies on default
installs.
Both fixes preserve every legitimate UX path. Origin-header gating is
transparent to browser tabs on the dashboard's own host, transparent
to Tauri/native shells (no Origin), and transparent to server-to-
server callers (no Origin). Only cross-origin browser fetches with a
foreign Origin lose the injection.
frontend/src/app/api/[...path]/route.ts
Adds isSameOriginOrNonBrowser() — checks the Origin header against
the request's own Host. Allow if no Origin (native/server-to-
server), allow if Origin host == Host host (same-origin), reject
otherwise. The admin-key injection now requires EITHER a valid
session cookie (auth) OR same-origin-or-non-browser (CSRF guard).
frontend/src/app/api/admin/session/route.ts
verifyAdminKey() simplified to local-only string comparison. When
ADMIN_KEY is configured, the supplied key must match exactly.
When ADMIN_KEY is unset, minting is refused entirely with a clear
message pointing the operator at the backend's auto-trust-loopback
behavior (SHADOWBROKER_TRUST_DOCKER_BRIDGE_LOCAL_OPERATOR=1, the
Docker default — local users keep working without a session).
The previous round-trip to /api/settings/privacy-profile was both
the source of the bug AND useless on its own merits (the endpoint
is public). Removing it makes the validation honest about what
it's checking.
Tests:
frontend/src/__tests__/proxy/proxyAuthBypassChain.test.ts (new, 12)
Cross-origin fetch to sensitive route → no admin-key injection
Cross-origin POST to sensitive route → no admin-key injection
Same-origin fetch → admin-key injection works
No-Origin (server-to-server / native) → admin-key injection works
Valid session cookie on cross-origin → cookie auth wins
Malformed Origin → conservative reject
Non-sensitive routes unaffected
Mint with ADMIN_KEY unset → refused (no fetch happens)
Empty key → 400
Mint with matching ADMIN_KEY → success
Mint with mismatched key → 403
Mint never round-trips to the backend (local-only validation)
frontend/src/__tests__/desktop/adminSessionBoundary.test.ts (updated)
Three tests updated to reflect the new local-only validation
contract. The previous tests asserted fetchMock.toHaveBeenCalled
which validated the now-removed (and broken) backend round-trip.
Full frontend suite: 707 passed, 72 files. No regressions.
Credit: @tg12 for the report. The cross-origin CSRF angle was
non-obvious — they specifically called out that the proxy's
admin-key injection was an open door for any page running in the
operator's browser, which is exactly the right framing.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
ShadowBroker Frontend
Next.js 16 dashboard with MapLibre GL, Cesium, and Framer Motion.
Development
npm install
npm run dev # http://localhost:3000
API URL Configuration
The frontend needs to reach the backend (default port 8000). Resolution order:
NEXT_PUBLIC_API_URLenv var — if set, used as-is (build-time, baked by Next.js)- Server-side (SSR) — falls back to
http://localhost:8000 - Client-side (browser) — auto-detects using
window.location.hostname:8000
Common scenarios
| Scenario | Action needed |
|---|---|
Local dev (localhost:3000 + localhost:8000) |
None — auto-detected |
LAN access (192.168.x.x:3000) |
None — auto-detected from browser hostname |
| Public deploy (same host, port 8000) | None — auto-detected |
Backend on different port (e.g. 9096) |
Set NEXT_PUBLIC_API_URL=http://host:9096 before build |
| Backend on different host | Set NEXT_PUBLIC_API_URL=http://backend-host:8000 before build |
Behind reverse proxy (e.g. /api path) |
Set NEXT_PUBLIC_API_URL=https://yourdomain.com before build |
Setting the variable
# Shell (Linux/macOS)
NEXT_PUBLIC_API_URL=http://myserver:8000 npm run build
# PowerShell (Windows)
$env:NEXT_PUBLIC_API_URL="http://myserver:8000"; npm run build
# Docker Compose (set in .env file next to docker-compose.yml)
NEXT_PUBLIC_API_URL=http://myserver:8000
Note: This is a build-time variable. Changing it requires rebuilding the frontend.
Theming
Dark mode is the default. A light/dark toggle is available in the left panel toolbar.
Theme preference is persisted in localStorage as sb-theme and applied via
data-theme attribute on <html>. CSS variables in globals.css define all
structural colors for both themes.