What is CSRF?

Cross-Site Request Forgery (CSRF) is a web security vulnerability that tricks an authenticated user's browser into sending unintended requests to a web application. The attacker exploits the trust that a website has in the user's browser, not the trust the user has in the website.

Unlike Cross-Site Scripting (XSS) which exploits the trust a user has for a particular site, CSRF exploits the trust a site has in a user's browser. When you are logged into a banking website, your browser automatically includes your session cookie with every request to that domain. An attacker can craft a malicious page that triggers requests to your bank, and your browser will dutifully attach your credentials.

💡
CSRF is also known as

You may see CSRF referred to as "session riding," "XSRF," or "one-click attack." The OWASP Top 10 has consistently listed it as a critical web application vulnerability.

How CSRF Attacks Work

A CSRF attack follows a predictable pattern. Understanding this chain is critical to building effective defenses.

1
The victim authenticates to a legitimate web application (e.g., their bank) and receives a session cookie
2
The attacker crafts a malicious page or email containing a hidden request to the target application
3
The victim visits the attacker's page (or opens the email) while still logged into the target application
4
The browser sends the forged request with the victim's session cookie automatically attached
5
The server processes the request as legitimate because the session cookie is valid

Here is what a simple CSRF attack looks like in practice. An attacker could embed this invisible form on any website:

<!-- Hidden on attacker's website -->
<form action="https://bank.example.com/transfer" method="POST" id="csrf-form">
    <input type="hidden" name="to_account" value="attacker-account-789" />
    <input type="hidden" name="amount" value="5000" />
</form>
<script>document.getElementById('csrf-form').submit();</script>

When the victim loads this page, the form auto-submits. Their browser sends the POST request to the bank with their active session cookie, and the transfer executes without the victim ever clicking a button.

⚠️
GET requests are even easier to exploit

If a state-changing action uses GET (e.g., /delete?id=42), the attacker only needs an image tag: <img src="https://target.com/delete?id=42">. This is why state-changing operations must never use GET requests.

Real-World CSRF Examples

CSRF vulnerabilities have been found in major applications throughout web security history, demonstrating that even well-resourced teams can overlook this class of attack.

  • Netflix (2006) - Attackers could change account email addresses and passwords through CSRF, effectively taking over accounts. The attack only required the victim to visit a malicious page while logged into Netflix.
  • ING Direct (2008) - A CSRF vulnerability allowed attackers to open additional accounts, transfer funds between the victim's accounts, and then withdraw from the newly created account.
  • YouTube (2008) - Researchers demonstrated CSRF attacks that could subscribe victims to channels, add videos to favorites, and send messages on behalf of the victim without any interaction.
  • Router administration panels - Many home routers have been vulnerable to CSRF attacks that change DNS settings, effectively redirecting all of the victim's traffic through an attacker-controlled server.
💡
CSRF and APIs

Traditional CSRF attacks target browser-based sessions with cookies. APIs that use token-based authentication (like Bearer tokens in the Authorization header) are generally not vulnerable to CSRF because the browser does not automatically attach these tokens. However, APIs that rely on cookies for authentication remain at risk.

CSRF Tokens

The most widely used defense against CSRF is the synchronizer token pattern. The server generates a unique, unpredictable token for each session (or each form), embeds it in the HTML, and validates it when the form is submitted. An attacker cannot read this token from a cross-origin page, so they cannot forge a valid request.

Server-Side Token Generation (Python/Flask example)

import secrets

@app.before_request
def generate_csrf_token():
    if 'csrf_token' not in session:
        session['csrf_token'] = secrets.token_hex(32)

@app.route('/transfer', methods=['POST'])
def transfer():
    token = request.form.get('csrf_token')
    if not token or token != session.get('csrf_token'):
        abort(403, 'CSRF token validation failed')
    # Process the legitimate request
    process_transfer(request.form)

HTML Form with CSRF Token

<form action="/transfer" method="POST">
    <input type="hidden" name="csrf_token" value="{{ session.csrf_token }}" />
    <label for="to_account">Recipient Account:</label>
    <input type="text" name="to_account" id="to_account" />
    <label for="amount">Amount:</label>
    <input type="number" name="amount" id="amount" />
    <button type="submit">Transfer</button>
</form>

CSRF Tokens for AJAX Requests

For JavaScript-driven applications, the token is typically sent in a custom HTTP header:

// Read token from meta tag or cookie
const csrfToken = document.querySelector('meta[name="csrf-token"]').content;

fetch('/api/transfer', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-CSRF-Token': csrfToken
    },
    body: JSON.stringify({ to_account: '12345', amount: 100 })
});

The server validates the X-CSRF-Token header against the session token. Cross-origin requests cannot set custom headers without CORS permission, which provides an additional layer of protection.

SameSite Cookies

The SameSite cookie attribute is a browser-level defense that controls whether cookies are sent with cross-site requests. It is one of the most effective modern mitigations against CSRF.

SameSite=Strict The cookie is never sent with cross-site requests. This provides the strongest CSRF protection but can affect usability (e.g., clicking a link from an email to your bank will not send the session cookie, requiring re-login).
SameSite=Lax The cookie is sent with top-level navigation GET requests (clicking a link) but not with cross-site POST requests, form submissions from other sites, or requests initiated by JavaScript. This is the default in most modern browsers.
SameSite=None The cookie is sent with all cross-site requests. Requires Secure flag. Only use this when cross-site cookie access is explicitly needed (e.g., embedded iframes, third-party integrations).
# Setting SameSite cookies (HTTP response header)
Set-Cookie: session_id=abc123; SameSite=Lax; Secure; HttpOnly; Path=/

# In Express.js
app.use(session({
    cookie: {
        sameSite: 'lax',
        secure: true,
        httpOnly: true
    }
}));

# In PHP
session_set_cookie_params([
    'samesite' => 'Lax',
    'secure'   => true,
    'httponly'  => true
]);
⚠️
SameSite alone is not sufficient

While SameSite=Lax blocks most CSRF attacks, it still allows GET-based CSRF through top-level navigation. If your application has any state-changing GET endpoints, SameSite=Lax will not protect them. Always combine SameSite cookies with CSRF tokens for defense in depth.

Defense in Depth

No single defense is bulletproof. A robust CSRF protection strategy layers multiple controls so that if one mechanism fails, others remain effective.

  • CSRF tokens - Include unique, per-session (or per-request) tokens in every state-changing form and validate them server-side
  • SameSite cookies - Set SameSite=Lax (or Strict) on all session cookies to prevent cross-site cookie transmission
  • Custom request headers - Require a custom header (e.g., X-Requested-With) on API calls; browsers block cross-origin custom headers without CORS
  • Origin/Referer validation - Check the Origin or Referer header to verify requests come from your own domain
  • Re-authentication for sensitive actions - Require password or MFA confirmation for high-impact operations like password changes or large transfers
  • Use POST for state changes - Never use GET requests for actions that modify data; this prevents trivial image-tag-based CSRF

Origin Header Validation Example

def validate_origin(request):
    allowed_origins = ['https://yourapp.example.com']
    origin = request.headers.get('Origin')
    referer = request.headers.get('Referer')

    # Origin header is present on POST/PUT/DELETE
    if origin:
        return origin in allowed_origins

    # Fall back to Referer for GET requests
    if referer:
        from urllib.parse import urlparse
        parsed = urlparse(referer)
        return f"{parsed.scheme}://{parsed.netloc}" in allowed_origins

    # If neither header is present, reject the request
    return False

Testing for CSRF

Testing your application for CSRF vulnerabilities is essential before deployment. Here are the key steps and tools to verify your defenses.

Manual Testing Checklist

  • Identify all state-changing endpoints (forms, API calls that modify data)
  • For each endpoint, attempt to submit a request from a different origin without a CSRF token
  • Verify that requests without valid CSRF tokens are rejected with a 403 status
  • Test whether tokens are tied to the session (try using a token from a different session)
  • Confirm that GET requests cannot trigger state changes
  • Check that SameSite cookie attributes are set correctly using browser developer tools

Using Browser Developer Tools

# In the browser console, check cookie attributes:
# Open DevTools > Application > Cookies
# Verify each session cookie shows:
#   SameSite: Lax (or Strict)
#   Secure: true (checkmark)
#   HttpOnly: true (checkmark)

# Test a forged request from the console:
fetch('https://yourapp.com/api/transfer', {
    method: 'POST',
    credentials: 'include',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ amount: 100 })
});
# This should be rejected (403) if CSRF protection is working

Automated Tools

  • OWASP ZAP - Free, open-source scanner that automatically identifies CSRF vulnerabilities in forms and AJAX endpoints
  • Burp Suite - Professional-grade tool with a CSRF PoC generator that creates attack pages to verify exploitability
  • CSRFTester - Dedicated OWASP tool for generating and testing CSRF proof-of-concept attacks
💡
Framework-level protection

Most modern web frameworks include built-in CSRF protection. Django has {% csrf_token %}, Rails has protect_from_forgery, Laravel has @csrf, and Express has csurf middleware. Always enable your framework's CSRF protection rather than building your own from scratch.

Summary

In this tutorial, you learned:

  • What CSRF is and why it exploits the browser's automatic cookie behavior
  • The step-by-step mechanics of a CSRF attack, from authentication to forged request
  • Real-world CSRF vulnerabilities found in Netflix, YouTube, and banking applications
  • How to implement CSRF tokens in both traditional forms and AJAX requests
  • The three SameSite cookie modes (Strict, Lax, None) and when to use each
  • Defense-in-depth strategies combining tokens, cookies, headers, and re-authentication
  • Manual and automated approaches to testing for CSRF vulnerabilities
🎉
You can now defend against CSRF attacks!

By combining CSRF tokens, SameSite cookies, and proper request validation, your web applications will be resilient against cross-site request forgery. Always test your defenses and use your framework's built-in protections when available.