Skip to main content

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.

  1. CSRF (Cross-Site Request Forgery) — abuse of automatically-sent cookies
  2. 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.

Bearer auth is resistant to CSRF

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.

ValueBehaviorUse
StrictNever sent cross-siteMost secure. Note: not even sent on first navigation from an external link
LaxSent only on top-level GET navigationsDefault. Balances usability and safety
NoneAlways 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.

OIDC redirects and SameSite

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:

  1. A cookie holding the secret (e.g. __Host-csrf) → should be HttpOnly (not readable by JS).
  2. The request token (the value the client returns in the X-CSRF-TOKEN header) → the SPA must be able to read it.
A common implementation mistake

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);
}
Beware of missed validation on reverse-proxy paths

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
DirectiveRole
default-src 'self'Allow only same-origin by default
script-srcLimit 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

HeaderRole
Strict-Transport-Security (HSTS)Enforce HTTPS; prevent downgrade / MITM
X-Content-Type-Options: nosniffDisable MIME sniffing
Referrer-Policy: no-referrerPrevent info leakage via the referrer
X-Frame-Options: DENYClickjacking 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.
  • HttpOnly cookies prevent token theft but not XSS → CSP and XSS prevention are mandatory.

References