2026-05-10 · 4 min
CSRF di SPA 2026: Yang Bener-bener Anda Butuhkan
Saya review code dari freelancer beberapa minggu ini. Pattern yang sering saya temukan: SPA yang implementasi CSRF token elaborate, padahal pakai SameSite=Lax cookie yang sudah handle 95% CSRF risk.
Mari kita clear-up.
Yang CSRF attack sebenarnya
Cross-Site Request Forgery: attacker site (evil.com) trigger request ke api Anda (yourapp.com) menggunakan kredensial user yang sudah login (cookie). Attacker tidak bisa lihat response (CORS protect), tapi bisa trigger side effect (delete account, transfer funds, etc).
Classic example:
<!-- di evil.com -->
<img src="https://yourapp.com/api/delete-account?confirm=yes" />
Browser akan attach cookie yang ada untuk yourapp.com. Server-side, request looks legit.
Defense modern (2026)
1. SameSite cookie attribute
Set cookie dengan SameSite=Lax (default browser modern):
response.cookies.set('session', token, {
httpOnly: true,
secure: true,
sameSite: 'lax', // <-- ini
path: '/',
});
Effect: cookie tidak attached pada cross-site request kecuali top-level navigation (link click). API request dari evil.com → tidak ada cookie.
SameSite=Strict lebih ketat tapi break user experience (cookie tidak attached saat user click link ke yourapp.com). Lax cukup.
2. Custom request header check
Browsers tidak ijinkan cross-origin request dengan custom header tanpa CORS preflight. Tambahkan check:
// middleware
app.use((c, next) => {
const customHeader = c.req.header('X-Requested-With');
if (!customHeader || customHeader !== 'XMLHttpRequest') {
return c.json({ error: 'invalid request' }, 403);
}
return next();
});
Client-side fetch:
fetch('/api/whatever', {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest',
},
});
Attacker yang trigger dari evil.com tidak bisa tambah header ini di simple request (akan trigger CORS preflight yang fail).
3. Origin / Referer header check
const allowed = new Set(['https://yourapp.com', 'https://www.yourapp.com']);
app.use((c, next) => {
if (c.req.method !== 'GET' && c.req.method !== 'HEAD') {
const origin = c.req.header('origin') || c.req.header('referer');
if (!origin || !allowed.has(new URL(origin).origin)) {
return c.json({ error: 'invalid origin' }, 403);
}
}
return next();
});
Origin header set by browser automatic untuk state-changing request. Attacker can’t forge.
Yang BUKAN modern (2026)
CSRF token per session
Pattern dari era PHP forms:
<form action="/transfer" method="POST">
<input type="hidden" name="_csrf" value="random-token-here" />
<input name="amount" />
</form>
Server validate _csrf token match cookie. Works, tapi tambah complexity untuk SPA:
- Generate token at session start
- Embed di every form / every fetch call
- Refresh token periodically
Setup overhead biasanya 100+ lines untuk full SPA.
Alternative SameSite + Origin check: 15 lines.
Double-submit cookie
Pattern complex: client harus baca cookie via JS, kirim sebagai header. Tapi: httpOnly cookie tidak bisa di-baca dari JS. Jadi: butuh cookie non-httpOnly. Reduces XSS protection.
Skip.
My pattern sekarang
// auth.middleware.ts
import { type MiddlewareHandler } from 'hono';
export const csrfProtect: MiddlewareHandler = async (c, next) => {
if (!['GET', 'HEAD', 'OPTIONS'].includes(c.req.method)) {
// Check Origin
const origin = c.req.header('origin') || c.req.header('referer');
const allowed = ['https://myapp.com', 'https://www.myapp.com'];
if (!origin || !allowed.includes(new URL(origin).origin)) {
return c.json({ error: 'csrf' }, 403);
}
}
await next();
};
// app.ts
app.use(csrfProtect);
8 lines middleware. Cookie set dengan SameSite=Lax di session creation.
Result: protected dari CSRF di 99% scenarios. Modern browser support: 100% dari Chrome 80+, Firefox 79+, Safari 13+. Indonesian user base mostly di mobile Chrome yang up-to-date.
Edge cases
iframe embedding
Kalau app Anda di-embed di iframe (mis. partner website): SameSite=None dengan Secure. Tapi pertimbangkan dulu apakah ini sebenarnya use case Anda. Most SMB SaaS tidak butuh iframe.
Mobile native app
Native app pakai header untuk auth (Bearer token), bukan cookie. CSRF tidak applicable.
Old browsers (IE 11, dll)
Set SameSite + fallback CSRF token. Tapi: per Februari 2026, IE 11 share di Indonesia ~0.04%. Skip.
Verdict
Stop implementing CSRF token elaborate. SameSite=Lax + Origin header check sudah cukup untuk 99% SPA Indonesia. Setup 15 menit. Save 200+ lines of code yang biasanya ada bug.
Ditulis oleh Reza Pradipta