CORS in fullstackhero has two non-default conventions: CORS middleware runs before HTTPS redirect (preflight requests can’t follow redirects), and credentialed CORS uses SetIsOriginAllowed rather than AllowAnyOrigin (the latter silently breaks SignalR). Security headers (CSP, HSTS, X-Frame-Options, Referrer-Policy) ship with sensible defaults and a few configurable knobs.
How the kit wires CORS
AddHeroCors binds CorsOptions from configuration and registers a single global policy (FSHCorsPolicy):
{ "CorsOptions": { "AllowAll": false, "AllowedOrigins": [ "https://app.example.com", "https://admin.example.com" ], "AllowedHeaders": [ "content-type", "authorization" ], "AllowedMethods": [ "GET", "POST", "PUT", "DELETE" ] }}AllowAll: true→SetIsOriginAllowed(_ => true)+ any header + any method +AllowCredentials(). Dev only.AllowAll: false→WithOrigins/WithHeaders/WithMethodsfrom the three lists +AllowCredentials(). Startup validation fails if any of the three lists is empty whileAllowAllis false.- Credentials are always allowed by the policy — there is no
AllowCredentialsconfig key. - If
AllowAllis false andAllowedOriginsis empty, CORS isn’t mounted at all — cross-origin browser calls will simply fail. (appsettings.Production.jsonships with an empty list precisely so you have to fill it in.)
UseHeroPlatform mounts the middleware before HTTPS redirect, because OPTIONS preflight requests cannot follow HTTP→HTTPS redirects per the Fetch spec — the redirect breaks the preflight and the actual request never goes out.
Pipeline order (relevant slice):
1. UseExceptionHandler2. UseResponseCompression3. UseCors ← before HTTPS redirect4. UseHttpsRedirection5. Security headers6. ...Why not AllowAnyOrigin for SignalR
CORS spec says: when a response has Access-Control-Allow-Credentials: true, the Access-Control-Allow-Origin must be an explicit origin, not *. SignalR’s negotiate request is credentialed (it carries Cookie or the JWT via accessTokenFactory’s query-param fallback). With AllowAnyOrigin(), the server emits Allow-Origin: *, which violates the spec — the browser silently refuses to use the response, and SignalR’s HubConnection fails to start with a confusing CORS error.
SetIsOriginAllowed(origin => true) echoes the actual origin back in Allow-Origin, satisfying the spec, while accepting every origin in practice (which is what dev wants).
Security headers
SecurityHeadersMiddleware emits the following on every response (except excluded paths):
| Header | Value | Purpose |
|---|---|---|
X-Content-Type-Options | nosniff | Prevent MIME-type sniffing |
X-Frame-Options | DENY | Prevent clickjacking via iframe |
Referrer-Policy | strict-origin-when-cross-origin | Limit referrer leakage |
X-XSS-Protection | 0 | Explicitly disable the legacy XSS auditor (modern guidance — CSP does this job) |
Strict-Transport-Security | max-age=31536000; includeSubDomains (HTTPS requests only) | Enforce HTTPS |
Content-Security-Policy | composed default, see below | Restrict what scripts, frames, images can load |
The CSP default (set only if nothing upstream already set one):
default-src 'self'; img-src 'self' data: https:;script-src 'self' https: {ScriptSources};style-src 'self' 'unsafe-inline' {StyleSources};object-src 'none'; frame-ancestors 'none'; base-uri 'self';Tune via SecurityHeadersOptions — the CSP isn’t free-form config; you get these knobs:
{ "SecurityHeadersOptions": { "Enabled": true, "ExcludedPaths": [ "/scalar", "/openapi" ], // docs UI manages its own scripts/styles "AllowInlineStyles": true, // drops 'unsafe-inline' from style-src when false "ScriptSources": [], // extra origins appended to script-src "StyleSources": [] // extra origins appended to style-src }}If you embed third-party scripts (analytics, customer-support widgets), add the origin to ScriptSources. For a different policy shape entirely (e.g. a connect-src restriction), set the Content-Security-Policy header yourself earlier in the pipeline — the middleware respects an existing header. Test with browser dev tools open — CSP violations log to the console.
Sub-resource integrity
CSP doesn’t enforce that third-party scripts haven’t been tampered with. For the few external scripts you’d ship (analytics, payment SDK), add integrity attributes:
<script src="https://cdn.example.com/widget.js" integrity="sha384-XYZ..." crossorigin="anonymous"></script>The browser verifies the hash before executing.
Cookies (if you swap JWT for cookie auth)
The kit defaults to JWT bearer (cookies not used for auth). If you fork to cookie auth, the standard hardening applies:
services.ConfigureApplicationCookie(o =>{ o.Cookie.HttpOnly = true; o.Cookie.SameSite = SameSiteMode.Strict; // or Lax for cross-site form-post flows o.Cookie.SecurePolicy = CookieSecurePolicy.Always; o.Cookie.Name = "fsh.auth";});The cookie should be HttpOnly (no JS access — limits XSS impact), Secure (HTTPS only), and SameSite=Strict for the strongest CSRF defence.
Common mistakes
- Setting
AllowAll = truein production. CORS exists to give browsers a sanity check on cross-origin calls. Opening to the world removes the check (it doesn’t directly compromise auth — auth still gates the request — but it removes the browser-enforced “is this site allowed to call you?” layer). - Forgetting to fill
AllowedOriginsin production. WithAllowAll: falseand no origins, CORS isn’t mounted — your React apps on other origins will get blocked by the browser. The symptom is “works in Postman, fails in the browser”. - Missing HSTS. Without HSTS, an attacker on the network can downgrade to HTTP for the first request. The kit emits it on HTTPS responses automatically; verify your proxy doesn’t strip it.
- CSP that breaks the UI. If a third-party widget breaks after tightening CSP, look at the browser console — CSP violations are logged. Add the needed origins to
ScriptSources/StyleSources, don’t disable the middleware.
Related
- Production checklist — header + CORS hardening items.
- Web building block —
AddHeroPlatform+UseHeroPlatformregistration. - Realtime — SignalR + credentialed CORS.