CORS and security headers
Allow browser front ends on other origins with exact-origin CORS, and the security headers Navree sends on every response.
On this page
Browsers block a page from reading responses from another origin unless the server allows it with CORS headers. Navree's CORS support is off until you list the origins of your front ends, and then allows exactly those, with credentials. Separately, every response carries a set of security headers that tell browsers not to sniff content types, frame the response or leak the referrer.
Both are part of HttpModule and configured in the http section.
CORS
Allowing origins
List the exact origins (scheme, host and port) of the front ends that call your API from the browser:
[http.cors]
allowed_origins = ["http://localhost:3000", "https://app.example.com"]APP_HTTP__CORS__ALLOWED_ORIGINS=http://localhost:3000,https://app.example.comWith the list empty, the default, Navree sends no CORS headers at all and browsers refuse cross-origin calls. Each entry must be an origin starting with http:// or https://; * and malformed entries fail the boot:
http.cors.allowed_origins must list exact origins; `*` is not allowedWhat Navree answers
For an allowed origin, a preflight request gets:
HTTP/1.1 200 OK
access-control-allow-origin: https://app.example.com
access-control-allow-credentials: true
access-control-allow-methods: GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS
access-control-allow-headers: accept,accept-language,authorization,content-type,if-match,if-none-match,traceparent,tracestate,x-csrf-token,x-request-id,x-requested-with,x-socket-id,x-xsrf-token
access-control-max-age: 600
vary: originand every actual response, errors included, gets:
access-control-allow-origin: https://app.example.com
access-control-allow-credentials: true
access-control-expose-headers: x-request-id,etag,location,retry-after,content-disposition
vary: origin- Origins are matched exactly. The response names the caller's origin, never
*. - Credentials (cookies,
Authorization) are allowed, sofetch(url, { credentials: "include" })works. - Methods:
GET,HEAD,POST,PUT,PATCH,DELETEandOPTIONS. - Request headers: the thirteen listed above, explicitly, since browsers don't accept a wildcard together with credentials. They cover bearer tokens, JSON bodies, conditional requests, tracing, request ids, the CSRF headers and the Pusher socket id.
- Exposed headers:
X-Request-Id,ETag,Location,Retry-AfterandContent-Dispositionare readable from JavaScript. - Preflight caching: browsers may reuse a preflight answer for 600 seconds.
Error responses from every layer carry the CORS headers too, including panics, timeouts, 429s and maintenance-mode 503s, so a front end can always read the error body. Preflight requests are answered before the rate limiter and don't count against it.
Options
| Key | Default | What it does |
|---|---|---|
http.cors.allowed_origins | [] (CORS off) | the exact origins allowed |
http.cors.allow_credentials | true | send Access-Control-Allow-Credentials: true |
http.cors.max_age_secs | 600 | how long browsers may cache a preflight answer |
The allowed methods, request headers and exposed headers are fixed: there are no keys to change them in this release.
When you don't need CORS
If your front end calls the API through its own server, as the CRM's Next.js backend-for-frontend does, the browser never makes a cross-origin call and you can leave the list empty. The CRM lists http://localhost:3000 anyway, for tools and direct calls during development.
Security headers
Navree adds these headers to every response:
| Header | Value | Effect |
|---|---|---|
X-Content-Type-Options | nosniff | browsers trust the Content-Type instead of guessing |
X-Frame-Options | DENY | the response can't be shown in a frame (clickjacking) |
Referrer-Policy | no-referrer | links from your responses don't leak their URL |
Strict-Transport-Security | max-age=31536000; includeSubDomains | browsers use HTTPS for your domain and its subdomains for a year |
Content-Security-Policy | default-src 'none'; frame-ancestors 'none'; base-uri 'none' | a response opened in the browser can't load scripts, styles or anything else |
The content security policy is strict because an API answers with JSON, not pages. Browsers ignore Strict-Transport-Security on plain HTTP, so it does no harm in development; once your domain is served over HTTPS with this header, browsers refuse plain HTTP to it and its subdomains for a year.
Changing a header for one response
Navree only adds a header when the response doesn't have it yet, so a value you set yourself wins. A route that serves HTML can send its own policy:
use navree::axum::http::header::{CONTENT_SECURITY_POLICY, CONTENT_TYPE};
use navree::axum::response::{IntoResponse, Response};
#[get("/status")]
#[public]
async fn status_page(&self) -> Response {
(
[
(CONTENT_TYPE, "text/html; charset=utf-8"),
(CONTENT_SECURITY_POLICY, "default-src 'self'"),
],
"<!doctype html><title>Status</title><p>All systems normal.</p>",
)
.into_response()
}The OpenAPI docs page at /api/docs does exactly this: it sends a policy that lets its script load.
Turning them off
[http]
security_headers = falseThis removes all five headers from every response, which makes sense when a proxy in front of the app sets them. There's no key to change a single default for every response; to do that, turn them off and have your proxy send your own set. Error responses keep X-Content-Type-Options: nosniff either way.
Testing
The test client sends Origin set to the first allowed origin on every request, so the CORS headers are there in tests. Check them, or the security headers, with header:
let response = app
.client()
.get("/api/v1/companies")
.header("origin", "https://app.example.com")
.send()
.await;
assert_eq!(
response.header("access-control-allow-origin"),
Some("https://app.example.com")
);
assert_eq!(response.header("x-frame-options"), Some("DENY"));