CSRF protection that AI-generated apps skip
作者:Seaworthy · 2026年6月17日
What CSRF actually does
Cross-site request forgery is one of those attacks that sounds academic until you see it hit a real app. The attacker doesn't need your user's password. They need your user to visit a malicious page while already logged into your app.
Here's the sequence: your user is authenticated on yourapp.com. A cookie sits in their browser proving that. The attacker tricks them into visiting evil.com, which contains a hidden form or a few lines of JavaScript that fire a request to yourapp.com/api/delete-account. The browser dutifully sends the session cookie along. Your server sees a valid authenticated request. It executes.
The attack works because browsers automatically attach cookies to cross-origin requests. That's the feature. That's also the problem.
Why AI-generated code gets this wrong
AI coding tools are good at the happy path. Ask for a form that transfers money, you get a form that transfers money. The authentication works. The validation works. CSRF protection is invisible, defensive infrastructure that doesn't appear in the feature description, so it gets skipped.
There's also a widespread and wrong assumption baked into a lot of AI-generated code: that JSON APIs are automatically safe from CSRF. They're not. The old argument was that browsers couldn't send cross-origin POST requests with Content-Type: application/json. That was never fully true, and with modern fetch() and permissive CORS configurations, it's thoroughly false.
What I see regularly in AI-generated Express and Next.js apps:
// This looks fine. It is not fine.
app.post('/api/settings/email', requireAuth, async (req, res) => {
const { newEmail } = req.body;
await db.user.update({
where: { id: req.user.id },
data: { email: newEmail }
});
res.json({ ok: true });
});
Session cookie? Present. Auth check? Present. CSRF protection? Absent. An attacker's page can submit a form to this endpoint and your server has no way to distinguish a legitimate request from your own frontend from a forged one.
The same pattern turns up in Django apps where @csrf_exempt gets added to silence a 403 error the AI didn't know how to fix, and in Laravel apps where VerifyCsrfToken gets quietly removed from the middleware kernel.
The SameSite shortcut and its limits
Modern browsers support the SameSite cookie attribute, which tells the browser not to send the cookie on cross-site requests. If your session cookie is SameSite=Strict, CSRF attacks are blocked in most browsers.
This is genuinely useful and you should be doing it. But it's not a complete solution:
- Older browsers don't support it, and enterprise environments can lag significantly
SameSite=Lax(the browser default in Chrome) still allows cookies on top-level navigations, so GET requests that mutate state are still vulnerable- If a subdomain is compromised,
SameSite=Laxdoesn't protect you
Set SameSite=Strict on your session cookie. Then add proper CSRF tokens on top. Both.
What proper CSRF protection actually looks like
The synchronizer token pattern is the standard approach. Your server generates a random token, stores it server-side, embeds it in your HTML forms or returns it in a response header, then validates it on every state-changing request.
In Express, the maintained replacement for the deprecated csurf package is csrf-csrf:
import { doubleCsrf } from 'csrf-csrf';
const { generateToken, doubleCsrfProtection } = doubleCsrf({
getSecret: () => process.env.CSRF_SECRET,
cookieName: '__Host-psifi.x-csrf-token',
cookieOptions: {
sameSite: 'strict',
secure: true,
httpOnly: true,
},
});
// Expose the token to your frontend
app.get('/api/csrf-token', (req, res) => {
res.json({ token: generateToken(req, res) });
});
// Protect all state-changing routes
app.use('/api', doubleCsrfProtection);
app.post('/api/settings/email', requireAuth, async (req, res) => {
// Only reachable if the CSRF token is valid
const { newEmail } = req.body;
await db.user.update({ where: { id: req.user.id }, data: { email: newEmail } });
res.json({ ok: true });
});
Your frontend fetches the token on load and includes it as a request header (X-CSRF-Token) on every mutating request. The middleware validates it against what's stored server-side. Requests without a matching token get rejected with a 403.
For Next.js App Router, the pattern is similar: fetch the token from an API route or server action, keep it in state, and send it with every form submission or fetch() call that changes data.
What to audit in your codebase
Look for state-changing routes (POST, PUT, PATCH, DELETE) that rely solely on a session cookie for authentication. If your middleware stack doesn't include CSRF validation, and you're not using stateless JWT tokens in Authorization headers, which are immune to CSRF because the browser doesn't auto-attach them, you have a gap.
Pay close attention to @csrf_exempt decorators in Django projects. They're sometimes added in tutorial code to silence a 403 without understanding why the 403 was there. The 403 was there for a reason.
Also audit GET routes. GET requests should never change state. If a quick route deletes or updates something on GET because it was simpler to link to, it's vulnerable even if CSRF tokens are otherwise in place.
Seaworthy flags missing CSRF middleware and @csrf_exempt usage as part of its security checks, so you get a ranked list of endpoints that need attention.
本文由 AI 生成,总结了公开可用的来源。
Seaworthy 会扫描你的代码仓库,发现类似本文提到的各种问题。
安全漏洞、泄露的密钥和配置错误——在部署前捕获。免费运行,无需账户。