← System Design Simulator

CSRF Attack + Defense

By Rahul Kumar · Senior Software Engineer · Updated · Category: Networking + Web Security

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.

CSRF Attack + Defense — Interactive Simulator

Runs fully client-side in your browser; no sign-up. Or open full screen →

Launch the interactive CSRF Attack + Defense widget — step through the algorithm or protocol and observe the internal state updating in real time.

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

CsrfFilter: synchronizer token + Origin check
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;
    }
}
Session cookie with SameSite=Lax + Secure + HttpOnly
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

Key design decisions & trade-offs

Common pitfalls

Interview follow-ups

Recommended reading

Related