CORS Configuration & Security
OmniRoute controls which browser origins may read cross-origin responses
from a single, centralized allowlist. The model is fail-closed by default:
no origin is allowed until you opt one in. This page documents how the allowlist
resolves, what CORS_ALLOW_ALL=true actually exposes (and, importantly, what it
does not), how to configure dev vs production safely, and the runtime warning
the dashboard shows when a wildcard is live.
Source of truth: src/server/cors/origins.ts (resolveAllowedOrigin,
applyCorsHeaders, getCorsStatus). The allowlist is applied once, in the
middleware (src/server/authz/pipeline.ts) — per-route handlers do not set
Access-Control-Allow-Origin themselves.
How an origin is resolved
Section titled “How an origin is resolved”For each request the middleware computes the Access-Control-Allow-Origin value
in this order:
CORS_ALLOW_ALL=true(or the legacyCORS_ORIGIN=*) → echo the caller’sOriginback (or*when there is noOriginheader), withVary: Originso caches stay correct. The sameapplyCorsHeaders()chokepoint also appendsVary: Accept-Encodingto every 2xx-with-body response on the token-authenticated/v1*//v1beta*surface (relaxForTokenAuth, RFC 9110 §12.5.5, issue #6737), so downstream/shared caches can correctly distinguish compressed vs uncompressed variants.- Otherwise, the request
Originis normalized (lower-cased, trailing slash stripped) and matched against the merged allowlist:- env
CORS_ALLOWED_ORIGINS— comma-separated list, and - the runtime
corsOriginssetting (Dashboard → Security → CORS Allowed Origins), injected viasetRuntimeAllowedOrigins()fromsrc/lib/config/runtimeSettings.ts.
- env
- No match → no
Access-Control-Allow-Originheader is emitted. The browser blocks the cross-origin read. This is the intended fail-closed default.
| Env var | Meaning |
|---|---|
CORS_ALLOWED_ORIGINS |
CSV of exact origins to allow (recommended). |
CORS_ALLOW_ALL |
true/1 → echo any origin (wildcard). Dev only. |
CORS_ORIGIN |
Legacy. * behaves like CORS_ALLOW_ALL; a single value is added to the allowlist. |
Threat model — what CORS_ALLOW_ALL=true really exposes
Section titled “Threat model — what CORS_ALLOW_ALL=true really exposes”The generic OWASP warning (“wildcard CORS = any site can call your API”) is worth taking seriously, but OmniRoute’s exposure is narrower than the generic case, because of one concrete implementation fact:
The central
applyCorsHeaders()never emitsAccess-Control-Allow-Credentials. A browser will not expose a credentialed (cookie-bearing) cross-origin response unless the server sendsAccess-Control-Allow-Credentials: true. OmniRoute’s shared CORS path never does.
What that means per surface, even with CORS_ALLOW_ALL=true:
| Surface | Auth mechanism | Effect of wildcard CORS |
|---|---|---|
Dashboard / MANAGEMENT /api/* |
Cookie session | Origin is echoed, but without Allow-Credentials the browser blocks the credentialed read. A malicious cross-origin site cannot read your authenticated dashboard responses, and the session cookie is not exposed. |
Client API /v1/*, /v1beta/* |
Bearer / x-api-key header |
Already permissive by design (relaxForTokenAuth): browsers never auto-attach Authorization/x-api-key, so an attacker’s page cannot supply your key. CORS_ALLOW_ALL does not widen this. |
Public read-only (/api/health, …) |
None | Non-sensitive; wildcard is harmless. |
So the residual exposure of CORS_ALLOW_ALL=true is limited to: (a)
non-credentialed cross-origin reads of already-unauthenticated data, and (b)
letting CORS preflight pass on management routes — which still require auth
that a cross-origin page cannot provide. It is not a session-hijack or
credential-theft vector on the shared CORS path.
One genuine exception — /api/v1/agents/
Section titled “One genuine exception — /api/v1/agents/”The Cloud-Agent routes (/api/v1/agents/{health,credentials,tasks,tasks/[id]}) set
their own CORS headers
(src/lib/cloudAgent/api.ts, getCloudAgentCorsHeaders) and do emit
Access-Control-Allow-Origin: <origin>|* together with
Access-Control-Allow-Credentials: true. This is the single surface where
origin-echo and credentials coexist, and it is independent of
CORS_ALLOW_ALL. These routes are management-authenticated
(requireManagementAuth); operators who expose the dashboard off-host should be
aware that this is the one place a cross-origin credentialed read is permitted by
the response headers. Tightening it to an explicit allowlist is tracked
separately from this CORS guidance.
Production checklist
Section titled “Production checklist”-
Never set
CORS_ALLOW_ALL=truein production. Leave it unset. -
Set an explicit origin list — either the env var or the Security-tab field:
Terminal window CORS_ALLOWED_ORIGINS="https://app.example.com, https://admin.example.com" -
If OmniRoute runs behind a reverse proxy / tunnel (nginx, Caddy, Cloudflare Tunnel, Tailscale), CORS is not your only control — the loopback route guard still protects spawn-capable routes (see ROUTE_GUARD_TIERS). Do not forge
X-Forwarded-For: 127.0.0.1to “fix” a 403; that re-opens the RCE class the route guard closes. -
Confirm the runtime state: the dashboard shows a persistent amber banner under Dashboard → Security → Authorization Inventory whenever
CORS_ALLOW_ALL=trueis live, and/api/settings/authz-inventoryreturns acors: { allowAll, allowedOrigins }envelope monitoring tools can poll.
Development convenience — allow specific local origins
Section titled “Development convenience — allow specific local origins”You rarely need the wildcard even in dev. Allow just the dev servers you use:
# Vite (5173) + Next.js (3000) dev servers calling a local OmniRouteCORS_ALLOWED_ORIGINS="http://localhost:5173, http://localhost:3000"Origins are matched case-insensitively with the trailing slash ignored, so
http://localhost:3000 and http://localhost:3000/ are equivalent. The same CSV
can be set at runtime in Dashboard → Security → CORS Allowed Origins without a
restart.
API keys vs cookie sessions
Section titled “API keys vs cookie sessions”- Bearer /
x-api-key(the/v1/*inference surface): browsers never attach these automatically. CORS is not a meaningful barrier here — the API key is the barrier — which is why that surface is intentionally permissive so browser and Electron clients can read responses they are already entitled to. - Cookie session (the dashboard): protected by the fail-closed default and
by the absence of
Access-Control-Allow-Credentialson the shared path. Keep management/dashboard origins out of any permissive config; they must stay exactly fail-closed.
Example: reverse proxy in front of OmniRoute
Section titled “Example: reverse proxy in front of OmniRoute”CORS is enforced by OmniRoute itself, so the proxy generally should not add or
rewrite Access-Control-* headers (double headers break browsers). Terminate TLS
and forward — let OmniRoute answer preflight:
# nginx — forward to OmniRoute; do NOT inject Access-Control-* herelocation / { proxy_pass http://127.0.0.1:20128; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; # Do NOT set X-Forwarded-For to 127.0.0.1 — it defeats the loopback route guard.}Set the allowed browser origins in OmniRoute (CORS_ALLOWED_ORIGINS or the
Security tab), not in the proxy.
Source files
Section titled “Source files”| Concern | File |
|---|---|
Allowlist resolution + getCorsStatus() |
src/server/cors/origins.ts |
| Middleware application (single source of truth) | src/server/authz/pipeline.ts |
| Settings → runtime origin injection | src/lib/config/runtimeSettings.ts |
| Runtime status for the dashboard | src/app/api/settings/authz-inventory/route.ts |
| Dashboard warning banner | src/app/(dashboard)/dashboard/settings/components/AuthzSection.tsx |
| CORS Allowed Origins field | src/app/(dashboard)/dashboard/settings/components/SecurityTab.tsx |
| Cloud-Agent per-route CORS (the exception) | src/lib/cloudAgent/api.ts |
See also
Section titled “See also”- Route Guard Tiers — loopback enforcement for spawn-capable routes (a separate, complementary control).
- Authorization Guide — the full auth pipeline.
HagiCode
HagiCode is an agentic coding workspace. Rich, convenient UI interactions make planning, agent execution, and project work approachable and easy to navigate, creating an engaging development experience.
Explore an engaging development experience through rich, convenient UI interactions.
- SmartOpenSpec workflows turn intent into an executable path, from proposal to design, tasks, and a shipped change.
- EfficientMulti-agent execution keeps research, implementation, and review moving in parallel, across several repositories and the Agent CLIs you already use.
- InterestingRich, convenient UI interactions make planning, agent execution, and project work approachable and easy to navigate.
Download for Windows
Share HagiCode
Turn ideas into shipped software.
HagiCode is an agentic coding workspace. OpenSpec workflows plan the change, multi-agent execution carries it out, and Hero Scenario makes the long run visible, so ideas become shipped software.
- Smart
- OpenSpec workflows turn intent into a path from proposal to shipped change.
- Efficient
- Multi-agent execution runs research, build, and review in parallel.
- Fun
- Rich UI interactions make planning, agent work, and projects easier to follow.
© 2026 HagiCode | Powered By hagilight-starlight@0.21.0 | Branch: main | Commit: de60bcd | Built:

