CSRF Explained: Cross-Site Request Forgery in Plain English

Share

TL;DR

CSRF tricks logged-in users into unknowingly performing actions on your site. When a user visits a malicious page, it can submit forms or make requests to your site using the user's existing session cookies. Protect against CSRF with SameSite cookies, CSRF tokens, and by checking the Origin header on sensitive requests.

What Is CSRF?

Cross-Site Request Forgery (CSRF, pronounced "sea-surf") is an attack that forces authenticated users to perform unwanted actions. Unlike XSS where attackers inject code into your site, CSRF uses the victim's own browser to make legitimate-looking requests.

Here's a simple scenario:

  1. You're logged into your banking website
  2. You visit a malicious site (or one that's been hacked)
  3. That site contains code that submits a money transfer request to your bank
  4. Your browser automatically includes your banking cookies
  5. The bank thinks you made the request and transfers the money

How CSRF Attacks Work

Basic Form Attack

Malicious page that transfers money
<!-- On evil-site.com -->
<html>
<body onload="document.forms[0].submit()">
  <form action="https://yourbank.com/transfer" method="POST">
    <input type="hidden" name="to" value="attacker-account" />
    <input type="hidden" name="amount" value="10000" />
  </form>
</body>
</html>

<!-- When a logged-in user visits this page,
     their browser submits the form with their cookies -->

Image-Based Attack

CSRF via image tag
<!-- GET requests can be triggered by images -->
<img src="https://yoursite.com/api/delete-account" style="display:none" />

<!-- Or change settings -->
<img src="https://yoursite.com/api/settings?email=attacker@evil.com" />

Why it works: Browsers automatically include cookies when making requests to a site, regardless of where the request originates. The server can't tell if the request came from your legitimate page or a malicious one.

Real-World CSRF Consequences

Attack TargetPotential Damage
Email settingsChange recovery email, take over account
Password changeLock user out of their account
Payment actionsTransfer money, make purchases
Admin functionsAdd attacker as admin, modify data
Social actionsPost content, follow accounts, share data

How to Prevent CSRF

1. Use SameSite Cookies

SameSite cookies won't be sent on cross-site requests:

Setting SameSite cookies
// Express.js session with SameSite
app.use(session({
  cookie: {
    httpOnly: true,
    secure: true, // Requires HTTPS
    sameSite: 'strict' // Or 'lax' for less strict protection
  }
}));

// Next.js API route
res.setHeader('Set-Cookie',
  'session=abc123; HttpOnly; Secure; SameSite=Strict');
SameSite ValueBehaviorUse Case
StrictNever sent cross-siteMaximum security
LaxSent on top-level navigations onlyGood balance (default in modern browsers)
NoneAlways sent (requires Secure)Third-party integrations

2. Use CSRF Tokens

Include a unique token in forms that must match the server's expected value:

CSRF token implementation
// Generate token and store in session
const csrfToken = crypto.randomUUID();
req.session.csrfToken = csrfToken;

// Include in form
<form action="/transfer" method="POST">
  <input type="hidden" name="_csrf" value="{csrfToken}" />
  <!-- other fields -->
</form>

// Verify on submission
if (req.body._csrf !== req.session.csrfToken) {
  return res.status(403).json({ error: 'Invalid CSRF token' });
}

3. Check Origin Header

Origin header validation
function validateOrigin(req, res, next) {
  const origin = req.headers.origin || req.headers.referer;
  const allowedOrigins = ['https://yoursite.com'];

  if (origin && !allowedOrigins.some(o => origin.startsWith(o))) {
    return res.status(403).json({ error: 'Invalid origin' });
  }
  next();
}

// Apply to state-changing routes
app.post('/api/*', validateOrigin);

4. Require Re-authentication for Sensitive Actions

Re-authentication for critical actions
// For sensitive actions, require password confirmation
app.post('/api/change-email', async (req, res) => {
  const { newEmail, currentPassword } = req.body;

  // Verify current password before making changes
  const isValid = await verifyPassword(req.user.id, currentPassword);
  if (!isValid) {
    return res.status(401).json({ error: 'Invalid password' });
  }

  // Now safe to change email
  await updateUserEmail(req.user.id, newEmail);
});

CSRF Protection in Frameworks

FrameworkBuilt-in ProtectionHow to Enable
Next.jsNone by defaultUse next-csrf or similar package
ExpressNone by defaultUse csurf middleware
DjangoEnabled by defaultInclude {% csrf_token %} in forms
RailsEnabled by defaultprotect_from_forgery included

Modern browsers help: Modern browsers default to SameSite=Lax for cookies without explicit SameSite setting, which prevents most CSRF attacks on POST requests.

What is CSRF?

CSRF (Cross-Site Request Forgery) is an attack that tricks a logged-in user into unknowingly submitting a request to a website. Because the user's browser automatically includes cookies, the site thinks the request is legitimate.

Do SameSite cookies prevent CSRF?

SameSite cookies provide good protection against CSRF. Setting SameSite=Strict prevents cookies from being sent on cross-site requests. SameSite=Lax allows cookies on top-level navigations but blocks them on cross-site POST requests.

Does using JSON APIs prevent CSRF?

JSON APIs have some natural CSRF protection because browsers won't send JSON in cross-origin requests without CORS approval. However, if your API accepts form-encoded data or has permissive CORS settings, it may still be vulnerable.

What's the difference between CSRF and XSS?

XSS injects malicious code into your website that runs in users' browsers. CSRF tricks users' browsers into making requests to your site. XSS exploits trust the user has in your site; CSRF exploits trust your site has in the user's browser.

Check Your CSRF Protection

Our scanner tests your forms and APIs for CSRF vulnerabilities.

Start Free Scan
Vulnerability Guides

CSRF Explained: Cross-Site Request Forgery in Plain English