CORS Policy
CORS Policy
This document describes the Cross-Origin Resource Sharing (CORS) policy enforced by the Bloqr Cloudflare Worker. It covers allowed origins, the preflight flow, no-origin requests, error responses, and Zero Trust integration.
Overview
All HTTP responses from the Worker include CORS headers computed at request time. The policy is strict by default:
- Only origins listed in
CORS_ALLOWED_ORIGINS(a Worker environment binding) receive a permissiveAccess-Control-Allow-Originheader. - Requests from unlisted origins are actively rejected with a
403 ProblemDetails(corsRejection) — the Worker does not silently omit the header. - Requests with no
Originheader (direct server-to-server, CLI, or Postman) are allowed through unconditionally. - Preflight
OPTIONSrequests return the full policy headers and204 No Content.
CORS Request Decision Flow
flowchart TD
A[Incoming Request] --> B{Origin header present?}
B -- No --> C[Allow: direct / server-to-server]
B -- Yes --> D{Origin in\nCORS_ALLOWED_ORIGINS?}
D -- Yes --> E[Set Access-Control-Allow-Origin\n= request Origin]
D -- No --> F[Return 403 ProblemDetails\ncorsRejection]
E --> G{Method = OPTIONS?}
G -- Yes --> I[Return 204 with\npreflight headers]
G -- No --> J[Continue to route handler\nwith CORS headers set]
Environment Binding
The allowed-origins list is configured via the CORS_ALLOWED_ORIGINS Worker binding (a comma-delimited string):
[vars]CORS_ALLOWED_ORIGINS = "https://app.bloqr.app,https://admin.bloqr.app"For local development, override via .dev.vars:
CORS_ALLOWED_ORIGINS = "http://localhost:4200,http://localhost:4201"Middleware Implementation
CORS is implemented in two steps inside worker/hono-app.ts:
Step 4 — Hono cors() middleware computes and attaches CORS response headers. For public endpoints (health, metrics, docs) it returns '*'; for all other endpoints it echoes the request Origin only when it matches the allowlist.
Step 4a — Origin enforcement middleware rejects disallowed origins with a 403 ProblemDetails response. Requests without an Origin header pass through unconditionally; public-endpoint requests also pass through.
// worker/hono-app.ts — Step 4: Hono cors() middlewareapp.use( '*', cors({ origin: (origin, c) => { const pathname = new URL(c.req.url).pathname; if (isPublicEndpoint(pathname)) return '*'; return matchOrigin(origin, c.env as Env) ?? undefined; }, allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], allowHeaders: [ 'Content-Type', 'Authorization', 'X-Turnstile-Token', 'X-Payg-Session', 'X-Payment-Response', 'X-Stripe-Customer-Id', ], exposeHeaders: ['X-Payg-Session-Remaining', 'X-Payment-Required', 'X-Request-Id'], maxAge: 86400, credentials: true, }),);
// worker/hono-app.ts — Step 4a: explicit origin enforcementapp.use('*', async (c, next) => { const origin = c.req.header('Origin');
// No Origin header → non-browser client → allow through unconditionally. if (!origin) { await next(); return; }
const pathname = c.req.path;
// Public endpoints allow any origin (wildcard * returned by step 4). if (isPublicEndpoint(pathname)) { await next(); return; }
const allowed = matchOrigin(origin, c.env as Env); if (!allowed) { analytics?.trackSecurityEvent({ eventType: 'cors_rejection', ... }); return ProblemResponse.corsRejection(pathname, `Origin '${origin}' is not permitted.`); }
await next();});The matchOrigin helper (in worker/utils/cors.ts) checks the request Origin against the comma-delimited CORS_ALLOWED_ORIGINS Worker binding.
Preflight OPTIONS Response Headers
A complete preflight response for an allowed origin:
HTTP/1.1 204 No ContentAccess-Control-Allow-Origin: https://app.bloqr.appAccess-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONSAccess-Control-Allow-Headers: Content-Type, Authorization, X-Turnstile-Token, X-Payg-Session, X-Payment-Response, X-Stripe-Customer-IdAccess-Control-Expose-Headers: X-Payg-Session-Remaining, X-Payment-Required, X-Request-IdAccess-Control-Allow-Credentials: trueAccess-Control-Max-Age: 86400Vary: OriginX-Turnstile-Token is retained in Access-Control-Allow-Headers for backward compatibility with older callers. The Angular frontend now injects the Turnstile token into the JSON request body (turnstileToken field) rather than as a header — see Turnstile Middleware for the current implementation.
No-Origin Requests
Requests without an Origin header — typically from:
- cURL / Postman / Newman (API testing)
- Server-to-server calls (CI pipeline, Worker-to-Worker)
- Cloudflare Cron Triggers
…are allowed through without any CORS header manipulation. The route handler processes them normally. No CORS headers are added to the response for these requests; this is intentional and correct.
Security note (ZTA): Absence of an
Originheader does not bypass authentication. All endpoints that require a session still invokeauth.api.getSession(). CORS is a browser enforcement mechanism — it does not substitute for server-side access control.
Error Responses
When a request arrives from a disallowed origin (i.e., the Origin header is present but not in CORS_ALLOWED_ORIGINS), the Worker returns a 403 Forbidden response with a ProblemDetails JSON body (ProblemResponse.corsRejection). The request is terminated — it does not proceed to the route handler.
For preflight (OPTIONS) requests from disallowed origins, the Worker likewise returns a 403 with no Access-Control-Allow-Origin header. The browser will not proceed with the actual request.
Adding a New Allowed Origin
- Update
CORS_ALLOWED_ORIGINSinwrangler.toml(for production) or.dev.vars(for local dev). - If the origin is on a new subdomain of
bloqr.app, ensure thebetter-authtrustedOriginsbinding (TRUSTED_ORIGINS) also includes it — see Better Auth Security Audit, AUDIT-11. - If the origin requires cookie sharing, verify the session cookie
Domainattribute is set to.bloqr.app.
Zero Trust Integration
| ZTA Principle | CORS Implementation |
|---|---|
| Verify explicitly | Every request from a browser origin is checked against the explicit allow-list |
| Least privilege | Access-Control-Allow-Origin is echoed only for approved origins |
| Never trust, always verify | Credentials (Authorization, session cookie) are still validated server-side regardless of CORS outcome |
| Short-lived access | Access-Control-Max-Age: 86400 caps preflight caching at 24 hours |
Related Documentation
- Turnstile Middleware — reads
turnstileTokenfrom the JSON request body;X-Turnstile-Tokenheader retained in CORS allowlist for backward compatibility - Better Auth Security Audit, AUDIT-11 —
trustedOriginswildcard finding - Worker Request Lifecycle — where CORS middleware fits in the pipeline
Bloqr AI™ — The privacy you didn't know you needed.
© 2026 Bloqr AI™, a trademark of Bloqr Systems™. Created by Bloqr Systems™, founded by Jayson Knight.
Internet Hygiene (n.) — the ongoing practices that keep your digital life clean, private, and safe.
Our product repos live in the BloqrAI org, part of the Bloqr Systems GitHub Enterprise. Product repos are internal-visibility — enterprise membership is required to view them.