karawaci.kode

2026-06-21 · 7 min

CSRF di SPA 2026: Masih Relevan?

Bulan lalu saya security review SaaS fintech klien Jakarta (regulator OJK, butuh annual security audit). CSRF dipertanyakan: “SameSite cookie sudah default Lax di browser modern, masih perlu token CSRF?” Saya audit 4 app klien saya yang ada paying customer + Indonesian financial context. Hasilnya bikin saya tetap implement CSRF protection.

Konteks teknikal

CSRF (Cross-Site Request Forgery) attack: penyerang trick user untuk submit request ke site target dengan cookie auth user. Cookie auto-attach by browser → server treat sebagai legitimate.

Defense lama:

  1. SameSite cookie: Strict / Lax / None. Lax sekarang default di Chrome, Firefox, Safari.
  2. CSRF token: per-session token yang harus included di POST/PUT/DELETE.
  3. Origin/Referer header check: server validate Origin.

Dengan SameSite=Lax default: cookie tidak dikirim di cross-site request kecuali top-level navigation GET. Apakah ini sudah cukup?

Audit 4 app klien

Saya pakai pendekatan: identify attack surface, lalu test exploit.

App 1: SaaS HR

  • Auth: Auth.js session cookie, SameSite=Lax (default)
  • CSRF token: tidak ada
  • Origin check: tidak ada

Saya coba: buat malicious page dengan auto-submit form POST ke /api/employees/delete. Browser kirim cookie (top-level POST? Tidak, hanya GET top-level. Form POST di iframe atau cross-site fetch: tidak kirim cookie).

Result: SameSite=Lax cukup proteksi standard form attack.

Tapi: saya temukan endpoint GET yang trigger destructive action (legacy code dari pre-2024):

GET /api/admin/disable-user?id=123

GET destructive = exploitable via <img src="..."> di malicious site. User klik link → auto-execute.

Risk: HIGH. Saya report ke klien, fix dengan migrate ke POST + audit semua endpoint untuk GET destructive.

App 2: Fintech (payment processing)

  • Auth: JWT di httpOnly cookie + Refresh token rotation
  • CSRF token: implemented (double-submit)
  • Origin check: yes, strict whitelist

Test:

  1. Form submit cross-origin → blocked (CSRF token mismatch + Origin reject)
  2. XHR cross-origin → blocked (CORS preflight reject)
  3. Subdomain attack: saya simulasi attacker.fintech-client.com (hypothetical takeover via wildcard DNS). Origin domain match SameSite policy (same-site karena same eTLD+1). Cookie kirim.

Subdomain takeover di Indonesia rare untuk corporate. Tapi audit found wildcard cert + DNS misconfiguration yang theoretically allow this. Saya rekomendasi tighten.

App 2 setup OK overall.

App 3: SaaS invoice

  • Auth: Session cookie SameSite=Lax
  • CSRF token: tidak ada
  • Server: Next.js Server Actions (which has built-in CSRF protection via Next-Action header)

Wait, Next.js Server Actions yang saya pakai claim CSRF protection automatic. Bagaimana?

Saya audit source: Server Actions verify Next-Action header dan Origin match. Origin reject for cross-site request. Implicit CSRF protection via Origin check + custom header (which CORS preflight enforce).

Test exploit:

  • Cross-site form submit dengan target Server Action: rejected karena tidak ada Next-Action header
  • Cross-site fetch with custom header: CORS preflight fail
  • Top-level navigation: SameSite=Lax block cookie

Verdict: Server Actions effectively immune ke standard CSRF. Tapi: kalau saya tambah API route REST yang share session cookie, exposure muncul lagi.

Untuk klien ini, app pure Server Actions: OK. Saya audit untuk memastikan tidak ada hybrid REST endpoint yang lupa CSRF.

App 4: POS warung (legacy)

  • Auth: session cookie, SameSite tidak set (default browser = Lax untuk Chrome 80+, tapi some browser pengguna pakai Samsung Internet versi lama)
  • CSRF token: tidak ada
  • Origin check: tidak ada

Browser fingerprint klien (analytics Plausible):

  • Chrome mobile: 68%
  • Samsung Internet: 14% (versi distribusi: 12% latest, 2% Samsung Internet 14 / Android 9 yang masih default ke SameSite=None)
  • Safari iOS: 11%
  • UC Browser: 4% (default SameSite=None di versi lama)
  • Lain: 3%

6% user app potensi vulnerable karena browser tidak default ke SameSite=Lax. Untuk 8400 user app: ~504 user.

Saya rekomendasi:

  1. Set SameSite=Lax explicit di cookie config (don’t rely on browser default)
  2. Implement double-submit CSRF token untuk POST endpoint
  3. Origin check untuk semua mutation endpoint

Implementation effort: 4 jam. Klien setuju.

Pattern 2026 yang saya pakai

Untuk app baru, pattern minimum:

// Set cookie explicit
Set-Cookie: session=abc; HttpOnly; Secure; SameSite=Lax; Path=/

// Middleware check
function csrfMiddleware(req) {
  if (['POST', 'PUT', 'DELETE', 'PATCH'].includes(req.method)) {
    // 1. Origin/Sec-Fetch-Site check
    const origin = req.headers.get('Origin') || req.headers.get('Referer');
    if (!origin || !isAllowedOrigin(origin)) {
      return new Response('Forbidden', { status: 403 });
    }
    
    // 2. Double-submit CSRF token (untuk REST endpoint)
    const cookieToken = getCookie(req, 'csrf-token');
    const headerToken = req.headers.get('X-CSRF-Token');
    if (!cookieToken || cookieToken !== headerToken) {
      return new Response('Forbidden', { status: 403 });
    }
  }
}

Plus untuk action critical (transfer dana, password change, account delete):

  • Explicit re-authentication step (re-enter password atau OTP)

Defense in depth.

Yang break

  1. Mobile app: Flutter / React Native client tidak punya browser CORS protection. CSRF token + Origin check tetap matter (Flutter manual attach token).

  2. Webhook receiver: endpoint POST yang trusted by external service (Midtrans webhook, WA Business webhook). Tidak boleh require CSRF token. Saya separate route prefix /api/webhook/* dengan signature verification HMAC sebagai gantinya.

  3. CSRF token rotation: kalau token bocor di logs / XSS, harus rotate. Saya tie token rotation ke session refresh.

  4. SPA + non-SPA route mix: Next.js app dengan page yang punya <form> non-JS submit (untuk progressive enhancement) — Server Action pattern handle ini OK. REST endpoint butuh manual CSRF token injection ke form.

Cost (engineering)

Untuk app baru: implementasi CSRF protection 4-8 jam. Bukan trivial, tapi tidak prohibitive.

Untuk audit + fix existing app: 1-3 hari per app, depending size.

Klien fintech bayar saya Rp 24jt untuk security audit + fix. Worth-it untuk peace of mind sebelum OJK audit.

Verdict

CSRF di 2026 masih relevan:

  • SameSite=Lax default helpful tapi tidak 100% (browser distribution Indonesia tidak monolitik)
  • GET destructive endpoint masih exist di legacy code
  • Subdomain takeover bukan paranoid scenario untuk perusahaan growing
  • Mobile native client tidak dapat browser protection

Defense-in-depth: SameSite + CSRF token + Origin check + re-auth untuk critical action.

Bukan magic — pattern lama tetap valid. Browser security feature additive, bukan replacement.

Pattern saya stick: implement CSRF protection di semua app dengan paying customer. Audit semester sekali. Untuk side project tanpa real consequences: SameSite default OK, tapi pattern muscle memory bagus untuk dipertahankan.

Untuk konteks browser dist Indonesia: jangan asumsi user pakai Chrome latest. Samsung Internet + UC Browser masih signifikan di SMB demographic.

Ditulis oleh Reza Pradipta