Repository navigation
fix: unify dashboard CSRF for dynamic public origins - #5856
diegosouzapw merged 1 commit into
Conversation
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
There was a problem hiding this comment.
Pull request overview
This PR extends the “dashboard CSRF repair” from a few test endpoints to the full dashboard management surface, so authenticated same-origin dashboard writes keep working when the UI is accessed through dynamic/ephemeral public hosts (tunnels, CDNs, HTTPS proxies) without requiring a pinned OMNIROUTE_PUBLIC_BASE_URL / NEXT_PUBLIC_BASE_URL.
Changes:
- Relax origin enforcement for
MANAGEMENTdashboard mutations by allowing a valid dashboard CSRF token to satisfy only theinvalid-originfailure case, while keeping Fetch Metadata (sec-fetch-site) fail-closed. - Install a dashboard-wide
fetchwrapper (viaDashboardLayout) to automatically attachx-omniroute-csrffor same-origin unsafe management mutations, and remove per-call-site CSRF header plumbing. - Update/expand unit tests and deployment docs to reflect the new behavior (dashboard writes no longer require a static public base URL; OAuth callbacks / generated links still may).
Validation / Coverage (per repo gate)
- Commands run in this review: none (not executed in this environment)
npm run test:coverage: not verified here (required gate per repo guidelines)- Changed/added test files:
tests/unit/ui/use-system-proxy-exit-guard.test.tsxtests/unit/ui/dashboard-csrf-fetch.test.tsxtests/unit/authz/pipeline.test.tstests/unit/authz/csrf.test.tssrc/app/(dashboard)/dashboard/providers/[id]/__tests__/useModelVisibilityHandlers.test.tsx
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
src/server/authz/pipeline.ts |
Allows dashboard CSRF token to bypass only invalid-origin for unsafe MANAGEMENT dashboard mutations; updates INVALID_ORIGIN messaging. |
src/server/authz/csrf.ts |
Removes path-limiting so dashboard CSRF tokens can validate for general management mutation paths (pipeline still gates usage). |
src/shared/utils/dashboardCsrf.ts |
Adds global dashboard fetch wrapper that auto-attaches CSRF header for same-origin unsafe management requests while excluding public/client API surfaces. |
src/shared/components/layouts/DashboardLayout.tsx |
Installs the dashboard CSRF fetch wrapper early in the dashboard layout lifecycle. |
src/app/(dashboard)/dashboard/tools/traffic-inspector/hooks/useSystemProxyExitGuard.ts |
Switches system-proxy revert-on-exit from sendBeacon to fetch(..., keepalive: true) to align with the CSRF mechanism. |
tests/unit/ui/use-system-proxy-exit-guard.test.tsx |
Updates hook-logic tests to assert keepalive fetch behavior instead of sendBeacon behavior. |
tests/unit/ui/dashboard-csrf-fetch.test.tsx |
Adds Vitest coverage for the new dashboard CSRF fetch wrapper behavior and exclusions. |
tests/unit/authz/pipeline.test.ts |
Expands authz pipeline tests to cover broader dashboard management routes + Fetch Metadata fail-closed behavior. |
tests/unit/authz/csrf.test.ts |
Updates CSRF token validation expectations to cover management mutation paths beyond the original test endpoints. |
src/app/(dashboard)/dashboard/combos/page.tsx |
Removes per-request CSRF header plumbing for combo tests (now provided by wrapper). |
src/app/(dashboard)/dashboard/providers/[id]/hooks/useModelVisibilityHandlers.ts |
Removes per-request CSRF header plumbing for model tests (now provided by wrapper). |
src/app/(dashboard)/dashboard/providers/[id]/components/PassthroughModelsSection.tsx |
Removes per-request CSRF header plumbing for model test-all (now provided by wrapper). |
src/app/(dashboard)/dashboard/providers/[id]/__tests__/useModelVisibilityHandlers.test.tsx |
Updates expectations after removal of per-call-site CSRF token plumbing. |
docs/reference/ENVIRONMENT.md |
Clarifies NEXT_PUBLIC_BASE_URL/public-origin semantics: dashboard writes use CSRF and don’t require a static public base URL. |
docs/ops/VM_DEPLOYMENT_GUIDE.md |
Updates reverse-proxy notes to reflect dashboard CSRF behavior. |
docs/ops/TUNNELS_GUIDE.md |
Adds explicit guidance that ephemeral tunnel hosts can still be used for normal dashboard writes after login. |
docs/guides/DOCKER_GUIDE.md |
Updates reverse-proxy guidance consistent with dashboard CSRF behavior. |
.env.example |
Updates comments to reflect dashboard CSRF protection vs. CORS/public-base-url requirements. |
| headers.set(DASHBOARD_CSRF_HEADER, token); | ||
| return originalFetch(input, { ...init, headers }); | ||
| }) as typeof fetch; | ||
| } |
| if (installCount === 0) { | ||
| originalFetch = globalThis.fetch.bind(globalThis) as typeof fetch; | ||
|
|
103a0ee
into
cheaper-inference:release/v3.8.43
|
Thanks @rdself! We reviewed the CSRF unification line by line — the fail-closed |
Integrated into release/v3.8.43
Problem
After the v3.8.40+ origin hardening, dashboard writes can fail behind changing public hosts unless
OMNIROUTE_PUBLIC_BASE_URL/NEXT_PUBLIC_BASE_URLis pinned to the exact browser URL. This breaks normal UI workflows such as combo/model tests, saving combo edits, provider/model edits, settings/UI configuration saves, and other dashboard management actions when the dashboard is reached through an ephemeral tunnel, CDN host, or HTTPS proxy.The v3.8.41 CSRF repair only covered the model/combo test endpoints. The rest of the dashboard still relied on public-origin matching, so authenticated same-origin UI writes could still return
INVALID_ORIGINeven though the user was using the dashboard normally.Root cause
Dashboard CSRF validation was path-limited to:
/api/combos/test/api/models/test/api/models/test-allThe frontend also attached the token only at those individual call sites. Regular management mutations such as
/api/settings,/api/combos/*,/api/providers/*,/api/quota/pools,/a2a, and other dashboard writes did not get the portable session-bound CSRF fallback.Changes
invalid-originbranch for authenticateddashboard_session+MANAGEMENT+ unsafe methods.sec-fetch-site: cross-siteis not bypassed by CSRF.DashboardLayout, before child passive effects can issue initial writes.x-omniroute-csrfautomatically to same-origin dashboard management mutations./api/v1*,/v1*,/v1beta*,/responses,/models,/codex, public auth/OAuth routes, and cross-origin requests)./a2a.sendBeacontofetch(..., keepalive: true)so it uses the same CSRF mechanism.Validation
node --import tsx/esm --test tests/unit/ui/use-system-proxy-exit-guard.test.tsx tests/unit/authz/csrf.test.ts tests/unit/authz/pipeline.test.tsnpx vitest run --config vitest.config.ts tests/unit/ui/dashboard-csrf-fetch.test.tsx 'src/app/(dashboard)/dashboard/providers/[id]/__tests__/useModelVisibilityHandlers.test.tsx' tests/unit/ui/use-model-visibility-handlers-test-all.test.tsxnpm run typecheck:corenpx eslint src/server/authz/csrf.ts src/server/authz/pipeline.ts src/shared/utils/dashboardCsrf.ts src/shared/components/layouts/DashboardLayout.tsx 'src/app/(dashboard)/dashboard/combos/page.tsx' 'src/app/(dashboard)/dashboard/providers/[id]/hooks/useModelVisibilityHandlers.ts' 'src/app/(dashboard)/dashboard/providers/[id]/components/PassthroughModelsSection.tsx' 'src/app/(dashboard)/dashboard/providers/[id]/__tests__/useModelVisibilityHandlers.test.tsx' 'src/app/(dashboard)/dashboard/tools/traffic-inspector/hooks/useSystemProxyExitGuard.ts' tests/unit/authz/csrf.test.ts tests/unit/authz/pipeline.test.ts tests/unit/ui/dashboard-csrf-fetch.test.tsx tests/unit/ui/use-system-proxy-exit-guard.test.tsxnpm run check:env-doc-syncnpm run check:fabricated-docsgit diff --check