CSRF Attack + Defense
Cross-site forgery. SameSite cookies, CSRF tokens, Origin checks.
This interactive explanation is built for system design interview prep: step through CSRF Attack + Defense, watch the internal state change, and connect the concept to real distributed-system trade-offs.
Overview
CSRF (Cross-Site Request Forgery) is the attack where a malicious site causes a victim's browser to make an authenticated request to your site that the victim did not intend. Because the browser automatically attaches cookies to requests for the target domain, a simple <form action='https://bank.example/transfer' method=POST> on evil.com can trigger a transfer from the victim who is currently logged into bank.example. The victim never clicks a link on bank.example; the request fires when they visit evil.com. CSRF is the mirror of XSS: XSS runs attacker code on your origin, CSRF runs your code (legitimate endpoints) with attacker parameters. Defenses have improved dramatically over the last few years. SameSite cookies block most cross-site sends by default, and CSRF tokens give you defense-in-depth for the remaining cases. Modern APIs that use Authorization: Bearer headers (not cookies) are naturally immune, because browsers do not attach bearer tokens cross-site.
How it works
A classic CSRF exploit works like this. The victim logs into bank.example, which sets a session cookie with the default (legacy) behavior of being sent on any cross-origin request. The victim visits evil.com, which contains <img src='https://bank.example/transfer?to=attacker&amount=1000'> or an auto-submitting form. The browser attaches bank.example's session cookie and makes the request. The server sees a valid session and processes the transfer. The defense stack has three layers. First, SameSite=Lax (the modern default) tells the browser to only send the cookie on same-site top-level navigations; cross-site image and XHR requests do not carry the cookie, which breaks the exploit without any server code. SameSite=Strict is safer but breaks cross-site login flows. SameSite=None requires Secure and is only for embedded widgets. Second, a synchronizer CSRF token: the server issues a random token, stores it in the session, and echoes it into every form and AJAX request. On POST, the server compares the token in the request to the session's token. An attacker cross-site cannot read the token (same-origin policy) so cannot forge a valid request. Third, origin checks: reject any state-changing request whose Origin or Referer header does not match the expected host. This catches the rare cross-site POST that slipped past SameSite. For APIs that authenticate via Authorization: Bearer, CSRF is usually a non-issue because browsers do not add bearer tokens cross-site automatically; only the app's own JS code sets them. The combination of SameSite=Lax cookies, a CSRF token for browser-form endpoints, and Origin checks on state-changing APIs covers virtually all exploitation paths.
Implementation
public class CsrfFilter implements Filter {
private static final SecureRandom RNG = new SecureRandom();
private static final String SESSION_ATTR = "csrfToken";
private static final String HEADER = "X-CSRF-Token";
private static final Set<String> SAFE = Set.of("GET", "HEAD", "OPTIONS");
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest http = (HttpServletRequest) req;
HttpServletResponse httpResp = (HttpServletResponse) resp;
HttpSession session = http.getSession(true);
String token = (String) session.getAttribute(SESSION_ATTR);
if (token == null) {
byte[] b = new byte[32];
RNG.nextBytes(b);
token = Base64.getUrlEncoder().withoutPadding().encodeToString(b);
session.setAttribute(SESSION_ATTR, token);
}
http.setAttribute("csrfToken", token); // expose to templates
if (!SAFE.contains(http.getMethod())) {
// 1. Origin / Referer must match our host.
String origin = http.getHeader("Origin");
String host = http.getServerName();
if (origin != null && !origin.endsWith("://" + host)) {
httpResp.sendError(403, "Origin mismatch");
return;
}
// 2. Synchronizer token must match session token.
String supplied = http.getHeader(HEADER);
if (supplied == null) supplied = http.getParameter("_csrf");
if (supplied == null || !constantTimeEquals(supplied, token)) {
httpResp.sendError(403, "Invalid CSRF token");
return;
}
}
chain.doFilter(req, resp);
}
private static boolean constantTimeEquals(String a, String b) {
if (a.length() != b.length()) return false;
int r = 0;
for (int i = 0; i < a.length(); i++) r |= a.charAt(i) ^ b.charAt(i);
return r == 0;
}
}
public class SessionCookieWriter {
public static void setSessionCookie(HttpServletResponse resp, String sessionId) {
// Servlet 5+ supports SameSite via ResponseCookie builder (Spring) or header.
String cookie = "JSESSIONID=" + sessionId
+ "; Path=/"
+ "; Secure" // only over HTTPS
+ "; HttpOnly" // unreadable by JS (XSS defense)
+ "; SameSite=Lax" // cross-site POSTs don't carry it
+ "; Max-Age=3600";
resp.addHeader("Set-Cookie", cookie);
}
/** For embedded widgets / third-party iframes that genuinely need cross-site cookies. */
public static void setEmbeddedCookie(HttpServletResponse resp, String name, String value) {
String cookie = name + "=" + value
+ "; Path=/"
+ "; Secure" // required with SameSite=None
+ "; HttpOnly"
+ "; SameSite=None";
resp.addHeader("Set-Cookie", cookie);
}
}
Complexity
- token generation:
O(1), 32 bytes random - token validation:
O(n) constant-time compare - SameSite cookie overhead:
zero server work
Key design decisions & trade-offs
- SameSite=Lax vs Strict — Chosen: Lax by default, Strict for high-value actions. Strict breaks cross-site login flows (clicking a link from email); Lax preserves UX while blocking most CSRF.
- Cookie vs bearer token auth — Chosen: Bearer for APIs, cookie for browser sessions. Bearer tokens are not auto-attached cross-site, which eliminates CSRF; cookies are simpler for traditional web apps.
- Per-session vs per-request token — Chosen: Per-session is usually enough. Per-request tokens add complexity (token rotation, race conditions) for marginal benefit when combined with SameSite.
Common pitfalls
- Using GET for state-changing operations — browsers send cookies on cross-site GETs
- Trusting the Referer header alone — it can be empty or spoofed by browser extensions
- Storing the CSRF token in a cookie that the attacker can set (double-submit without integrity)
- Forgetting CSRF on custom subdomains — SameSite=Lax is per registered domain
- Exempting 'SameSite=None' from CSRF tokens — that is the one case you need them most
Interview follow-ups
- Adopt SameSite=Lax as the default for all session cookies
- Add CORS preflight enforcement for non-simple state-changing APIs
- Move browser auth to short-lived cookie + refresh token (BFF pattern)
- Log 403 CSRF failures and alert on spikes
Recommended reading
- Alex Petrov, Database Internals — storage engines and distributed systems internals.
- Martin Kleppmann, Designing Data-Intensive Applications (DDIA) — data models, replication, partitioning, consistency.
- The System Design Primer — high-level design building blocks.
- Foundational networking + web-security references (TCP/IP, TLS 1.3, OWASP Top 10).