Your form sends data to the server. Your user is authenticated. An attacker, with a simple link or a crafted image, can force that request without the user's knowledge. That's CSRF (Cross-Site Request Forgery), and if you don't block it, you leave a door wide open. We at Meteora Web see it every day in projects that come to us: unprotected forms, APIs accepting requests without tokens, exposed sessions. The fix is simple and ready to use. Let's talk.
Why is CSRF protection in forms and APIs still essential today?
CSRF works because the browser automatically includes session cookies in requests to the destination domain. If a user is logged into yourbank.com and clicks a malicious link on another site, the browser sends the request with session cookies. Without a verification token, the server cannot tell that the request originated from a different context.
A classic example: changing a password via a GET request without confirmation. Or transferring funds. In modern applications, forms and APIs are the main vectors. Ignoring CSRF means giving control of your application to anyone who can write a fake <img> tag.
Protection relies on a shared secret — the anti-CSRF token — generated by the server, which the client must include in every state-changing request. The server verifies the token. Simple, effective, but only if applied correctly.
What does it cost not to protect?
We come from accounting: a successful CSRF attack can mean data modification, account theft, insertion of malicious content. The cost of implementation? A few lines of code. The cost of an incident? Potentially thousands of euros and reputation. We think in terms of the client's numbers: security is an investment with a very high ROI, not an expense.
Sponsored Protocol
How does CSRF protection in HTML forms work with synchronous tokens?
The most common method, the Synchronizer Token Pattern, works like this:
- The server generates a random token and associates it with the user's session.
- The token is inserted into the form as a hidden field.
- On submission, the server compares the received token with the one stored in the session.
- If they match, the request is valid. Otherwise, it's rejected.
Here's an implementation in plain PHP:
// Generation and storage
session_start();
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
?>
<form method="post" action="/update">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<!-- other fields -->
<button type="submit">Save</button>
</form>
<?php
// Verification
if ($_POST['csrf_token'] !== $_SESSION['csrf_token']) {
die('Invalid request');
}
?>
In Laravel, the mechanism is automatic thanks to the VerifyCsrfToken middleware. Just add @csrf in your Blade form. To exclude certain routes (e.g., external webhooks), configure them in the $except array of the middleware.
Sponsored Protocol
// App/Http/Middleware/VerifyCsrfToken.php
protected $except = [
'webhook/*',
];
Watch out for double submit
If your server doesn't maintain sessions (e.g., stateless API), you cannot store the token in a session. The solution is the Double Submit Cookie: generate a token, write it to a non-httpOnly cookie (yes, deliberately accessible to JS), and also send it as a header or form field. The server compares the cookie value with the received one.
Example API implementation with Express.js:
const crypto = require('crypto');
// Generate token on login
app.post('/login', (req, res) => {
const token = crypto.randomBytes(32).toString('hex');
res.cookie('csrf_token', token, { httpOnly: false, sameSite: 'strict' });
// Also send in response
res.json({ csrfToken: token });
});
// Protection middleware
function csrfProtection(req, res, next) {
const cookieToken = req.cookies.csrf_token;
const headerToken = req.headers['x-csrf-token'];
if (!cookieToken || !headerToken || cookieToken !== headerToken) {
return res.status(403).json({ error: 'CSRF token mismatch' });
}
next();
}
// Protected route
app.post('/transfer', csrfProtection, (req, res) => {
// transfer
});
How to ensure CSRF protection in REST APIs without session state?
Stateless APIs, often using JWT tokens, have no server-side session. Here, the Double Submit Cookie is the most practical solution. Alternatively, you can use the SameSite cookie attribute. Setting SameSite=Strict or Lax makes the browser block cross-site requests. But it's not sufficient for all cases: Strict also blocks normal links from other sites. The best approach is to combine SameSite with a token.
Sponsored Protocol
For public APIs using API keys (not cookies), CSRF is not a concern because there are no session cookies. The CSRF vector exists only when authentication is cookie-based.
Example in Laravel for stateless APIs
Laravel excludes API routes from CSRF middleware by default. But if you use session tokens on API routes, you must protect them. Solution: use Sanctum with SPA — it uses session cookies and provides automatic CSRF protection via the session token.
// config/sanctum.php
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', 'localhost,localhost:8000')),
// On login, Sanctum sends an 'XSRF-TOKEN' cookie (encrypted)
// The frontend must include it in every request as header 'X-XSRF-TOKEN'
// Axios does this automatically if configured with withCredentials: true
If you're building a React/Vue app consuming Laravel APIs with sessions, you need to fetch the CSRF token from /sanctum/csrf-cookie and then send it with every request.
Sponsored Protocol
What common mistakes to avoid in CSRF protection in forms and APIs?
We've seen many:
- Predictable tokens: using
md5(time())or short strings. Userandom_bytes()oropenssl_random_pseudo_bytes()with 32 bytes. - Reusable tokens: some frameworks regenerate the token on every request (e.g., Laravel does by default). Others don't. If you don't regenerate, a stolen token is valid for the entire session. Better to regenerate after each POST request.
- GET requests with side effects: never perform state changes via GET. If you do, CSRF cannot be blocked with tokens (attackers can trigger GET via
<img>tags). Rewrite those routes to POST/PUT/DELETE. - Forgetting APIs: many applications protect forms but leave APIs exposed. If your APIs use session cookies, they need the same protection.
- Redundant double protection: if you use SameSite=Strict and tokens, it's overkill but not harmful. But if the token is in an httpOnly cookie, you can't read it from JS, so you can't send it as a header. In that case, use Double Submit Cookie with a JS-accessible cookie.
How to test CSRF protection on your site?
Practical tools:
- Burp Suite: intercept the request, remove the token, see if the server accepts it.
- CURL: build a cross-origin request with a simple HTML form and check if it's blocked.
- Browser extensions: tools like CSRF Tester generate payloads automatically.
Quick test: open your browser console on a target site, run:
Sponsored Protocol
// Try to send a POST request without token
fetch('https://yoursite.com/update', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ data: 'test' })
}).then(r => console.log(r.status));
If it returns 200, CSRF protection is missing or misconfigured.
What to do now
- Check every form and every POST/PUT/DELETE route: verify that a valid CSRF token exists and is being verified, not just present.
- Regenerate the token after each write request: if using a framework, confirm it's the default behavior. In Laravel, yes; in WordPress, the nonce is regenerated only on certain actions.
- Protect APIs that use session cookies: if you have a SPA with Sanctum or similar, make sure the frontend sends the correct token.
- Set SameSite=Lax or Strict on session cookies. It's an extra layer that blocks many cross-site requests.
- Run a security audit: if you lack time, contact us. We always start with one question: how much does it cost and how much does it return? Security is not optional.
For a full overview of web security, visit our Pillar Guide on Web Security for Developers. You'll also find other spokes on XSS, SQL Injection, HTTPS, and server hardening.
Official reference: OWASP CSRF.