CSRF / SameSite / CSP and XSS Defense
Overview
Cookie-based authentication (BFF pattern or HTTP-only Cookie Authentication) is a strong approach that keeps tokens out of the browser, but because cookies are sent automatically, defenses against the following two threats are mandatory.
- CSRF (Cross-Site Request Forgery) — abuse of automatically-sent cookies
- XSS (Cross-Site Scripting) — session hijacking by injected script (session riding)
This document organizes three defenses — the SameSite attribute, antiforgery tokens, and CSP (Content Security Policy) — in the context of cookie authentication.
CSRF (Cross-Site Request Forgery)
CSRF makes a logged-in user's browser send an unintended request from an attacker's site. Because cookies are automatically attached to the destination origin, even a POST from an attacker page carries the victim's session cookie.
Sending a Bearer token explicitly in an Authorization header is
implicitly CSRF-resistant, because the header is not auto-attached cross-site. Cookie auth, by
contrast, requires explicit CSRF defense.
Defense 1: the SameSite attribute
A cookie's SameSite attribute controls whether the cookie is sent on cross-site requests.
| Value | Behavior | Use |
|---|---|---|
Strict | Never sent cross-site | Most secure. Note: not even sent on first navigation from an external link |
Lax | Sent only on top-level GET navigations | Default. Balances usability and safety |
None | Always sent (Secure required) | Only when cross-site embedding is required |
SameSite=Strict is a strong first line of defense against CSRF, but is considered insufficient on its
own (browser differences, future spec changes, same-site paths). Combine it with antiforgery tokens
for defense-in-depth.
Even when using SameSite=Strict on the auth cookie, the correlation cookie used in the OIDC callback
(a top-level navigation from the IdP) may need SameSite=None or similar. ASP.NET Core's OIDC handler
handles these automatically, but take care in custom implementations.
Defense 2: antiforgery (CSRF) tokens
A server-issued, unpredictable token is validated on state-changing requests (POST/PUT/DELETE/PATCH). Two common patterns:
- Synchronizer token pattern: the server holds a session-bound token and compares it with the request token.
- Double-submit cookie: the token is sent in both a cookie and a header, and the two are checked for a match.
Separate the HttpOnly secret from the JS-readable request token
ASP.NET Core antiforgery consists internally of two parts:
- A cookie holding the secret (e.g.
__Host-csrf) → should beHttpOnly(not readable by JS). - The request token (the value the client returns in the
X-CSRF-TOKENheader) → the SPA must be able to read it.
Making the antiforgery cookie HttpOnly is the correct setting. But because the SPA cannot read an
HttpOnly cookie, you must separately emit a JS-readable request-token cookie (e.g. XSRF-TOKEN,
HttpOnly=false). Dropping HttpOnly to work around this is wrong. In ASP.NET Core, issue the
request token via IAntiforgery.GetAndStoreTokens().
// Emit a JS-readable request token on safe (GET) responses
app.Use(async (ctx, next) =>
{
if (HttpMethods.IsGet(ctx.Request.Method))
{
var tokens = antiforgery.GetAndStoreTokens(ctx);
ctx.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken!, new CookieOptions
{
HttpOnly = false, // the SPA must read this one
Secure = true,
SameSite = SameSiteMode.Strict
});
}
await next();
});
// SPA side: read XSRF-TOKEN and send it as the X-CSRF-TOKEN header
export function csrfInterceptor(req, next) {
if (['POST', 'PUT', 'DELETE', 'PATCH'].includes(req.method)) {
const token = getCookie('XSRF-TOKEN'); // non-HttpOnly cookie
if (token) req = req.clone({ headers: req.headers.set('X-CSRF-TOKEN', token) });
}
return next(req);
}
ASP.NET Core's UseAntiforgery() auto-validates only endpoints carrying antiforgery metadata.
Reverse-proxy (e.g. YARP) proxy endpoints do not carry that metadata, so validation may not fire on
unsafe methods to /api/**. You must call IAntiforgery.ValidateRequestAsync() explicitly before
proxying.
Defense 3: XSS and CSP (Content Security Policy)
Token theft vs. session riding
Cookie + HttpOnly prevents token theft (exfiltration) but does not prevent XSS itself. An
injected script cannot read the cookie, but because the cookie is auto-sent on same-origin requests, the
script can operate the API in the victim's session (session riding).
Therefore fundamental XSS prevention is mandatory, and CSP is at its core.
Key CSP directives
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-<random>'; # allow inline scripts only via nonce/hash
object-src 'none';
base-uri 'self';
frame-ancestors 'none'; # clickjacking defense
| Directive | Role |
|---|---|
default-src 'self' | Allow only same-origin by default |
script-src | Limit executable script sources. Avoid 'unsafe-inline'/'unsafe-eval'; use nonce/hash |
object-src 'none' | Forbid <object>/<embed> |
base-uri 'self' | Prevent link hijacking via <base> tampering |
frame-ancestors 'none' | Forbid embedding in iframes from other sites |
Security headers to combine
| Header | Role |
|---|---|
Strict-Transport-Security (HSTS) | Enforce HTTPS; prevent downgrade / MITM |
X-Content-Type-Options: nosniff | Disable MIME sniffing |
Referrer-Policy: no-referrer | Prevent info leakage via the referrer |
X-Frame-Options: DENY | Clickjacking defense for older browsers (complements CSP frame-ancestors) |
Summary
- Cookie auth sends cookies automatically, so CSRF defense is mandatory.
- Combine SameSite=Strict (first line) with antiforgery tokens (defense-in-depth).
- Antiforgery separates the "HttpOnly secret" from the "JS-readable request token."
- CSRF validation is easily missed on reverse-proxy paths, so validate explicitly.
HttpOnlycookies prevent token theft but not XSS → CSP and XSS prevention are mandatory.
Related documents
- BFF Pattern and the Token-Protection Security Model
- HTTP-only Cookie Authentication
- Bearer Authentication
- OWASP Top 10