Skip to content
bitzorcas
中EN

Guide

CORS, trusted proxies, and security headers

Configure and verify browser origins, trusted proxy topology, and defensive response headers at the API boundary.

Last updated

CORS, forwarded headers, and security response headers solve different problems. CORS controls whether a browser may expose a cross-origin response. Forwarded Headers controls whether the application trusts the client IP and scheme reported by a proxy. Security headers constrain how a browser interprets a response.

Host pipeline order

The relevant API order is UseForwardedHeaders → ForwardedPortHostMiddleware → exception/correlation handling → UseCors → Scalar CSP when documentation is enabled → SecurityHeadersMiddleware → authentication. Forwarded Headers must run before components that use RemoteIpAddress or IsHttps; public-port recovery belongs immediately after trusted proxy processing; CORS belongs before authentication.

SLB / CDN / OpenResty
│ X-Forwarded-For / X-Forwarded-Proto / X-Forwarded-Host / X-Forwarded-Port
▼
trusted proxy validation → public-port recovery → CORS preflight → security headers → auth → tenant

The Gateway also parses trusted proxies and emits security headers. The middleware never overwrites an existing header, so the edge may set a stricter policy while the API still protects controlled direct access.

Configure production CORS

List complete production origins. Missing AllowedMethods or AllowedHeaders means any method or header after an origin is accepted. PreflightMaxAgeHours has a minimum of one hour. Credentials are enabled only when explicitly configured.

{
"Cors": {
"AllowedOrigins": [
"https://console.example.com",
"https://portal.example.com"
],
"AllowedMethods": [
"GET",
"QUERY",
"POST",
"PUT",
"PATCH",
"DELETE",
"OPTIONS"
],
"AllowedHeaders": [
"Authorization",
"Content-Type",
"X-Client-Platform",
"traceparent",
"correlationId",
"Idempotency-Key"
],
"AllowCredentials": true,
"PreflightMaxAgeHours": 1
}
}

An origin contains scheme, host, and port, not a path. With no origin, the policy denies cross-origin access without preventing startup. Development defaults to any origin only when AllowAnyOrigin is not explicitly set; production does not.

QUERY is not a CORS-safelisted method, so paged, list, and complex read requests start with an OPTIONS preflight. If QUERY is missing from the method allowlist, @bitz/platform-sdk sees a browser network error and attempts POST /_query. That preserves availability but can hide an edge-configuration gap. Release evidence must still prove native QUERY through the real CDN, WAF, ingress, and gateway. See the QUERY protocol for both transport paths.

Configure trusted proxies

The API processes X-Forwarded-For, X-Forwarded-Proto, and X-Forwarded-Host; the adjacent port middleware processes X-Forwarded-Port only when its trust condition holds. Together, these values shape HTTPS, Cookie Origin, and Scalar debug Server Scheme/Host. ForwardedHeaders:ForwardLimit controls how many hops are accepted: repo appsettings often default to 1; OpenResty → Gateway → API preview chains should use 2. Known proxy collections are cleared and rebuilt from configuration.

{
"ForwardedHeaders": {
"ForwardLimit": 2,
"KnownProxies": ["10.20.0.10", "127.0.0.1"],
"KnownNetworks": ["10.30.0.0/16"],
"DirectExposure": false
}
}

Invalid IP addresses or CIDRs fail configuration. Production and Staging must provide a known proxy/network or explicitly declare DirectExposure=true; otherwise the runtime guard refuses startup.

Trust only the immediate infrastructure hop. Trusting arbitrary public X-Forwarded-* headers lets an attacker forge client IP, Host, or port data used by rate limiting, IP filtering, audit evidence, login Cookie Origin, and documentation debug addresses. Browser refresh-cookie Origin checks use only Scheme://Host after trusted forwarded-header and port correction; unverified raw headers are not treated as the trusted Origin.

OpenResty/Nginx should populate Host and X-Forwarded-Host from $http_host, then send X-Forwarded-Port $server_port. $host can discard a non-default port such as 8088, causing Scalar calls to fall back to 80/443. See OpenAPI and Scalar documentation surface for configuration and fallback behavior.

Configure security headers

SecurityHeadersMiddleware observes reloadable Security:Headers options. Defaults enable HSTS, nosniff, frame denial, a strict referrer policy, a same-origin CSP, and denial of geolocation, microphone, and camera.

{
"Security": {
"Headers": {
"Enabled": true,
"EnableHsts": true,
"HstsMaxAgeSeconds": 63072000,
"HstsIncludeSubDomains": true,
"HstsPreload": false,
"ContentTypeOptions": "nosniff",
"FrameOptions": "DENY",
"ReferrerPolicy": "strict-origin-when-cross-origin",
"ContentSecurityPolicy": "default-src 'self'; script-src 'self'; connect-src 'self' https://api.example.com",
"PermissionsPolicy": "geolocation=(), microphone=(), camera=()"
}
}
}

HSTS is emitted only when the corrected request is HTTPS, HSTS is enabled, and max-age is positive. Prove every subdomain supports HTTPS before enabling includeSubDomains or preload.

Tighten CSP deliberately

Inventory actual front-end dependencies, then grant script, style, image, font, and connection sources separately. The default keeps 'unsafe-inline' for styles as an SPA compatibility trade-off; it is not the strongest possible policy.

Collect violation evidence in a non-blocking environment before enforcement. Do not use default-src *, leave 'unsafe-eval' open, or broaden every directive to fix one third-party component.

Verify the real edge

Terminal window
# Inspect headers through the actual CDN/SLB/Gateway path.
curl -sS -D - -o /dev/null https://api.example.com/health/live
# Send a legal browser preflight and inspect the access-control response.
curl -i -X OPTIONS https://api.example.com/api/tickets \
-H 'Origin: https://console.example.com' \
-H 'Access-Control-Request-Method: QUERY' \
-H 'Access-Control-Request-Headers: authorization,content-type,x-client-platform'

Capture evidence through both the public edge and any supported direct API route. A Gateway response does not prove the API’s direct-access defense is configured.

Contract-test denial

Test allowed and unknown origins, credential mode, HTTPS/HSTS, and the non-overwrite behavior for an existing stricter header.

// An unknown Origin may receive an HTTP response, but no CORS grant.
request.Headers.Add("Origin", "https://evil.example");
request.Headers.Add("Access-Control-Request-Method", "QUERY");
var response = await client.SendAsync(request);
// Absence of the allow-origin header is the denial evidence.
response.Headers.TryGetValues("Access-Control-Allow-Origin", out _)
.ShouldBeFalse();

Diagnose failures

For a CORS error, inspect the OPTIONS request, exact Origin, requested method, and requested headers. For redirect loops or missing HSTS, verify that the trusted proxy corrected Request.Scheme. For blocked assets, inspect the exact CSP directive in browser diagnostics.

Postman cannot prove CORS because it is not a browser enforcement agent. Likewise, a correct CORS policy does not prove trusted proxy or CSP behavior.

Release evidence

  • Production/Staging passes the proxy-topology startup guard;
  • legal and illegal origins have preflight contract tests;
  • HSTS, CSP, frame, MIME, referrer, and permissions policies are captured at the public edge;
  • rate limiting and audit use a validated client address;
  • each CSP exception has an owner and removal condition;
  • Gateway and API headers do not conflict or weaken one another.

See also

100%

Scroll or use controls to zoom · drag when enlarged · double-click for 100% / 200%