CSRF #

Cross-Site Request Forgery (CSRF) adalah serangan di mana website yang dikontrol attacker membuat browser korban mengirimkan request ke website yang sudah dipercaya — dan karena browser secara otomatis menyertakan cookie sesi, server tidak bisa membedakan apakah request ini benar-benar berasal dari keinginan user atau dari halaman jahat yang dikunjungi user.

Yang membuat CSRF berbeda dari serangan lain: attacker tidak perlu mengetahui password, tidak perlu mencuri cookie, tidak perlu memiliki akses ke akun korban. Mereka hanya perlu korban membuka sebuah halaman — bisa lewat link di email, postingan di media sosial, atau situs yang sering dikunjungi yang sudah dikompromikan — sementara korban masih dalam keadaan login di aplikasi target.

CSRF memanfaatkan kepercayaan yang sudah ada: browser mempercayai server, server mempercayai browser, dan cookie menjadi “bukti” kepercayaan itu. CSRF membalikkan kepercayaan ini: attacker menggunakan kepercayaan server terhadap browser korban untuk melakukan tindakan yang tidak pernah dimaksudkan oleh korban.

Cara Kerja CSRF: Dari Perspektif Attacker #

sequenceDiagram
    participant V as Victim Browser
    participant E as evil.com
    participant B as bank.com

    V->>B: Login ke bank.com
    B->>V: Set-Cookie: session=VALID_TOKEN

    Note over V: Korban masih login di bank.com<br/>Cookie session masih aktif

    V->>E: Buka evil.com (klik link dari email/chat)
    E->>V: Response HTML dengan hidden form

    Note over V,E: Form otomatis di-submit oleh JavaScript
    V->>B: POST /transfer {to: "attacker", amount: 10000000}
           Cookie: session=VALID_TOKEN (dikirim otomatis oleh browser!)

    Note over B: Bank.com menerima request dengan session valid
    B->>B: Validasi session ✓, Eksekusi transfer ✗
    B->>V: 200 OK — Transfer berhasil

    Note over V: Korban tidak sadar terjadi transfer

Kunci dari serangan ini: browser mengirimkan cookie secara otomatis ke domain yang sesuai, tanpa peduli dari mana request itu dipicu. Request yang dipicu oleh evil.com ke bank.com tetap menyertakan cookie bank.com — karena itu adalah perilaku browser yang sudah ada sejak awal, bukan bug.

<!-- Payload CSRF paling sederhana — hidden form yang auto-submit -->
<!-- Di halaman evil.com -->

<html>
<body onload="document.forms[0].submit()">
  <!-- Form ini tidak terlihat oleh korban -->
  <form action="https://bank.com/transfer" method="POST" style="display:none">
    <input type="hidden" name="to" value="attacker_account">
    <input type="hidden" name="amount" value="10000000">
  </form>
</body>
</html>

<!-- Korban yang membuka halaman ini dengan session bank.com yang aktif
     akan langsung mengirim transfer tanpa perlu mengklik apapun -->
<!-- CSRF via img tag — untuk GET request (harusnya tidak ada state-changing GET) -->
<img src="https://bank.com/logout" style="display:none">
<!-- Setiap kali halaman dimuat, request GET ke /logout dikirim -->
<!-- Ini kenapa state-changing action tidak boleh menggunakan GET -->

<!-- CSRF via fetch (hanya berhasil jika tidak ada CORS restriction yang ketat) -->
<script>
fetch('https://bank.com/transfer', {
  method: 'POST',
  credentials: 'include',  // sertakan cookie
  headers: {'Content-Type': 'application/x-www-form-urlencoded'},
  body: 'to=attacker&amount=10000000'
})
</script>
<!-- fetch dengan Content-Type selain JSON tidak memerlukan preflight CORS
     dan bisa berhasil jika server tidak memvalidasi Origin -->

Mengapa CORS Saja Tidak Cukup Melindungi dari CSRF #

Banyak developer mengira CORS sudah melindungi dari CSRF. Ini kesalahpahaman yang berbahaya.

Apa yang CORS lindungi:
  → Mencegah JavaScript di evil.com MEMBACA response dari bank.com
  → Preflight request mencegah beberapa jenis custom request

Apa yang CORS TIDAK lindungi:
  → Form submission biasa (HTML form) tidak tunduk pada CORS
  → Browser tetap mengirim request — CORS hanya mencegah JavaScript membaca response-nya
  → Untuk CSRF, attacker tidak perlu membaca response — mereka hanya perlu
    request dikirim dan dieksekusi

Contoh yang masih rentan meski ada CORS:
  <!-- Form biasa TIDAK memerlukan CORS preflight -->
  <form action="https://bank.com/transfer" method="POST">
    <input name="amount" value="10000000">
  </form>

  <!-- fetch dengan simple content-type juga tidak memerlukan preflight -->
  fetch(url, {
    method: 'POST',
    body: 'amount=10000000',
    credentials: 'include',
    headers: {'Content-Type': 'application/x-www-form-urlencoded'}
  })

Tiga Strategi Utama CSRF Protection #

Strategi 1: Synchronizer Token Pattern (CSRF Token) #

Ini adalah strategi paling kuat dan paling universal. Server generate token random yang unik per sesi (atau per request untuk keamanan lebih tinggi), menyisipkannya ke dalam form, dan memverifikasinya saat request diterima. Attacker tidak bisa mengetahui token ini karena mereka tidak bisa membaca halaman bank.com dari evil.com (blocked by same-origin policy).

# Flask — implementasi CSRF token yang benar
import secrets
import hmac
import hashlib

def generate_csrf_token():
    """Generate CSRF token baru dan simpan di session."""
    token = secrets.token_urlsafe(32)  # 256-bit random token
    session['csrf_token'] = token
    return token

def validate_csrf_token(token_from_request):
    """Validasi CSRF token dari request."""
    expected_token = session.get('csrf_token')

    if not expected_token or not token_from_request:
        return False

    # Gunakan hmac.compare_digest untuk mencegah timing attack
    # (mencegah attacker menebak token karakter per karakter berdasarkan waktu)
    return hmac.compare_digest(expected_token, token_from_request)

# Decorator untuk melindungi endpoint
from functools import wraps

def csrf_protect(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if request.method in ('POST', 'PUT', 'PATCH', 'DELETE'):
            # Coba ambil token dari header (untuk AJAX) atau form field
            token = (request.headers.get('X-CSRFToken') or
                    request.form.get('csrf_token') or
                    request.json.get('csrf_token') if request.is_json else None)

            if not validate_csrf_token(token):
                return jsonify({'error': 'CSRF validation failed'}), 403
        return f(*args, **kwargs)
    return decorated_function

# Endpoint yang dilindungi
@app.route('/transfer', methods=['POST'])
@csrf_protect
def transfer():
    # Hanya bisa dicapai jika CSRF token valid
    amount = request.form['amount']
    to = request.form['to']
    # Process transfer...
<!-- Template HTML — sisipkan CSRF token di setiap form -->
<!-- Jinja2 -->
<form method="POST" action="/transfer">
    <!-- Token dirender di hidden input -->
    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
    <input type="number" name="amount" placeholder="Jumlah">
    <input type="text" name="to" placeholder="Nomor rekening tujuan">
    <button type="submit">Transfer</button>
</form>
// Untuk AJAX request — kirim CSRF token sebagai header
// Ambil token dari meta tag atau cookie (yang tidak HttpOnly)

// Cara 1: dari meta tag di HTML
const csrfToken = document.querySelector('meta[name="csrf-token"]').content;

// Cara 2: dari cookie (token disimpan di cookie biasa, bukan HttpOnly)
function getCsrfToken() {
    return document.cookie
        .split('; ')
        .find(row => row.startsWith('csrf_token='))
        ?.split('=')[1];
}

// Kirim sebagai header di setiap request yang memodifikasi data
async function postData(url, data) {
    const response = await fetch(url, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'X-CSRFToken': getCsrfToken(),  // header CSRF token
        },
        body: JSON.stringify(data),
        credentials: 'include'  // sertakan session cookie
    });
    return response.json();
}

// Setup global untuk axios
axios.defaults.headers.common['X-CSRFToken'] = getCsrfToken();

SameSite adalah atribut cookie yang menginstruksikan browser untuk tidak mengirim cookie pada cross-site request. Ini adalah pertahanan CSRF yang paling bersih karena bekerja di level browser tanpa memerlukan perubahan besar di aplikasi.

Tiga nilai SameSite dan efeknya terhadap CSRF:

SameSite=Strict:
  Cookie TIDAK dikirim pada cross-site request apapun.
  → Klik link dari email ke bank.com: cookie tidak dikirim, user perlu login
  → Hidden form dari evil.com: cookie tidak dikirim → CSRF gagal ✓
  → Terlalu ketat untuk banyak use case (login flow rusak)

SameSite=Lax (rekomendasi untuk sebagian besar kasus):
  Cookie dikirim hanya pada top-level navigation dengan GET method.
  → Klik link → bank.com: cookie dikirim (user tidak perlu login ulang) ✓
  → Hidden form POST dari evil.com: cookie TIDAK dikirim → CSRF gagal ✓
  → Fetch/XHR cross-site: cookie TIDAK dikirim ✓
  → Tidak mencegah: form GET dari cross-site (tapi GET harusnya read-only)

SameSite=None; Secure:
  Cookie dikirim pada semua cross-site request.
  → Tidak ada perlindungan CSRF
  → Hanya gunakan untuk third-party widget, SSO lintas domain
# Set session cookie dengan SameSite=Lax
response.set_cookie(
    'session',
    value=session_token,
    httponly=True,
    secure=True,
    samesite='Lax',   # perlindungan CSRF dasar
    max_age=28800
)
SameSite=Lax tidak sepenuhnya melindungi jika aplikasi menggunakan GET untuk state-changing operations. Selalu pastikan operasi yang mengubah data menggunakan POST, PUT, PATCH, atau DELETE — bukan GET. SameSite adalah defense in depth, bukan pengganti CSRF token untuk aplikasi yang membutuhkan keamanan tinggi.

Double submit cookie adalah alternatif CSRF token yang berguna ketika server tidak bisa memaintain state per-session (misalnya arsitektur stateless atau multiple server instance tanpa shared session store).

Caranya: server set CSRF token di cookie biasa (bukan HttpOnly) dan minta client mengirimkannya kembali di header atau form field. Server memverifikasi bahwa nilai di cookie sama dengan nilai di request body/header.

# Implementasi Double Submit Cookie

# Server: set cookie saat page load
@app.route('/dashboard')
def dashboard():
    # Generate CSRF token dan set sebagai cookie biasa (bukan HttpOnly)
    csrf_token = secrets.token_urlsafe(32)
    response = make_response(render_template('dashboard.html'))
    response.set_cookie(
        'csrf_token',
        csrf_token,
        secure=True,
        samesite='Lax',
        # HttpOnly=False (default) — perlu bisa dibaca JavaScript
    )
    return response

# Server: validasi double submit
def validate_double_submit(request):
    cookie_token = request.cookies.get('csrf_token')
    header_token = request.headers.get('X-CSRFToken')

    if not cookie_token or not header_token:
        return False

    # Keduanya harus sama
    return hmac.compare_digest(cookie_token, header_token)

@app.route('/transfer', methods=['POST'])
def transfer():
    if not validate_double_submit(request):
        return jsonify({'error': 'CSRF validation failed'}), 403
    # Process transfer...
// Client: baca CSRF token dari cookie dan kirim sebagai header
function getCSRFToken() {
    const cookies = document.cookie.split(';');
    for (const cookie of cookies) {
        const [name, value] = cookie.trim().split('=');
        if (name === 'csrf_token') {
            return decodeURIComponent(value);
        }
    }
    return null;
}

// Kirim sebagai header di setiap modifying request
async function apiRequest(method, url, data) {
    return fetch(url, {
        method,
        headers: {
            'Content-Type': 'application/json',
            'X-CSRFToken': getCSRFToken(),  // nilai dari cookie
        },
        body: data ? JSON.stringify(data) : undefined,
        credentials: 'include'
    });
}
Mengapa Double Submit Cookie berhasil:

  Attacker dari evil.com tidak bisa membaca cookie csrf_token dari bank.com
  (Same-Origin Policy mencegah ini)

  Attacker bisa mengirim request ke bank.com dengan cookie otomatis,
  TAPI tidak bisa membaca nilai csrf_token untuk dikirim sebagai header

  Server memverifikasi: cookie_token == header_token
  → Jika attacker kirim request tanpa header: validasi gagal ✓
  → Jika attacker tebak token: probabilitas 2^256 → mustahil ✓

  Kelemahan: rentan jika ada subdomain yang bisa set cookie ke parent domain
  → Gunakan bersama cookie prefix __Host- untuk mitigasi ini

Validasi Origin dan Referer Header #

Sebagai lapisan pertahanan tambahan, server bisa memvalidasi bahwa request berasal dari domain yang benar berdasarkan header Origin atau Referer.

ALLOWED_ORIGINS = {
    'https://app.contoh.com',
    'https://www.contoh.com',
}

def validate_request_origin(request):
    """
    Validasi bahwa request berasal dari domain yang diizinkan.
    Ini adalah defense in depth — bukan pengganti CSRF token.
    """
    # Cek header Origin terlebih dahulu (lebih reliable)
    origin = request.headers.get('Origin')
    if origin:
        return origin in ALLOWED_ORIGINS

    # Fallback ke Referer
    referer = request.headers.get('Referer')
    if referer:
        from urllib.parse import urlparse
        parsed = urlparse(referer)
        referer_origin = f"{parsed.scheme}://{parsed.netloc}"
        return referer_origin in ALLOWED_ORIGINS

    # Jika tidak ada Origin maupun Referer:
    # Ini bisa terjadi pada beberapa browser atau konfigurasi
    # Putuskan berdasarkan kebutuhan keamanan aplikasi
    # Untuk aplikasi sensitif: tolak
    # Untuk aplikasi umum: pertimbangkan izinkan dengan mitigasi lain
    return False

@app.before_request
def check_origin():
    if request.method in ('POST', 'PUT', 'PATCH', 'DELETE'):
        if not validate_request_origin(request):
            return jsonify({'error': 'Invalid request origin'}), 403
Keterbatasan validasi Origin/Referer:

  ✓ Efektif untuk sebagian besar serangan CSRF
  ✗ Beberapa browser atau privacy tools menghapus header Referer
  ✗ Origin header tidak ada pada beberapa form submission
  ✗ Tidak cukup sebagai satu-satunya pertahanan

  Gunakan sebagai:
  → Lapisan tambahan DI SAMPING CSRF token, bukan pengganti

CSRF di Arsitektur SPA (Single Page Application) #

SPA dengan token JWT di header Authorization secara natural lebih aman dari CSRF — karena form HTML biasa tidak bisa menambahkan custom header. Tapi ada nuansa yang perlu dipahami:

CSRF dan SPA — kapan perlu khawatir:

  Pola aman dari CSRF:
  ┌────────────────────────────────────────────────────────┐
  │ SPA menyimpan JWT di memory (bukan localStorage/cookie)│
  │ Setiap request manual menambahkan header Authorization │
  │ → Form dari evil.com tidak bisa menambahkan header ini │
  │ → CSRF tidak bisa terjadi                              │
  └────────────────────────────────────────────────────────┘

  Masih rentan CSRF jika:
  ┌────────────────────────────────────────────────────────┐
  │ SPA menggunakan cookie untuk autentikasi (HttpOnly)    │
  │ Browser otomatis mengirim cookie ke bank.com           │
  │ → Request dari evil.com menyertakan cookie ini         │
  │ → CSRF bisa terjadi seperti biasa                      │
  │ → Butuh CSRF token atau SameSite=Strict/Lax            │
  └────────────────────────────────────────────────────────┘

  Strategi terbaik untuk SPA dengan cookie autentikasi:
  1. SameSite=Lax untuk session cookie (perlindungan dasar)
  2. CSRF token untuk operasi yang sangat sensitif (transfer, delete)
  3. Konfirmasi ulang password untuk operasi destruktif (hapus akun, dll)
// Contoh setup CSRF protection untuk SPA React/Vue/Angular

// 1. Ambil CSRF token dari server saat aplikasi load
async function initApp() {
    const response = await fetch('/api/csrf-token', {
        credentials: 'include'
    });
    const { csrfToken } = await response.json();

    // Simpan di memory (bukan localStorage)
    window.__csrfToken = csrfToken;
}

// 2. Interceptor untuk semua request API
// Axios
axios.interceptors.request.use(config => {
    if (['post', 'put', 'patch', 'delete'].includes(config.method)) {
        config.headers['X-CSRFToken'] = window.__csrfToken;
    }
    return config;
});

// Fetch wrapper
async function secureApi(method, url, data) {
    const options = {
        method: method.toUpperCase(),
        credentials: 'include',
        headers: {
            'Content-Type': 'application/json',
        }
    };

    if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(options.method)) {
        options.headers['X-CSRFToken'] = window.__csrfToken;
    }

    if (data) {
        options.body = JSON.stringify(data);
    }

    return fetch(url, options);
}

Kapan Setiap Strategi Digunakan #

Decision guide untuk memilih strategi CSRF protection:

  Aplikasi tradisional dengan form HTML:
  → Synchronizer Token Pattern (CSRF token di hidden input)
  → Tambahkan SameSite=Lax pada session cookie
  → Validasi Origin/Referer sebagai lapisan tambahan

  SPA dengan cookie-based auth:
  → SameSite=Lax pada session cookie (minimum)
  → CSRF token via header (X-CSRFToken) untuk operasi sensitif
  → Double submit cookie jika server stateless

  API yang diakses dari multiple frontend:
  → Validasi Origin header dengan whitelist
  → Jika menggunakan JWT di header Authorization: tidak perlu CSRF token
    (header custom tidak bisa ditambahkan oleh form biasa)

  Operasi sangat sensitif (hapus akun, transfer besar):
  → Tambahkan konfirmasi password ulang
  → CSRF token + konfirmasi password = defense in depth yang kuat

  Multi-domain (SSO, embed widget):
  → SameSite=None; Secure (CSRF protection dari token)
  → CSRF token wajib karena SameSite tidak bisa melindungi

Anti-Pattern yang Harus Dihindari #

# ✗ Anti-pattern 1: GET untuk state-changing operation
@app.route('/delete-user')
def delete_user():
    user_id = request.args.get('id')
    User.query.filter_by(id=user_id).delete()
    return redirect('/')
# Attacker: <img src="https://app.com/delete-user?id=123">
# → User dihapus setiap kali halaman dengan img tersebut dibuka

# ✓ Solusi: hanya POST/DELETE untuk operasi yang mengubah data

────────────────────────────────────────────────────────────────────────────

# ✗ Anti-pattern 2: CSRF token yang tidak di-validate di server
@app.route('/transfer', methods=['POST'])
def transfer():
    # CSRF token dikirim tapi tidak divalidasi!
    token = request.form.get('csrf_token')  # diabaikan
    amount = request.form.get('amount')
    # Process transfer...

# ✓ Solusi: validasi token sebelum memproses request

────────────────────────────────────────────────────────────────────────────

# ✗ Anti-pattern 3: CSRF token yang sama untuk semua user
STATIC_CSRF_TOKEN = "hardcoded_token_12345"

@app.route('/transfer', methods=['POST'])
def transfer():
    if request.form.get('csrf_token') != STATIC_CSRF_TOKEN:
        abort(403)
# Token yang sama untuk semua user bisa ditebak atau bocor dari satu user

# ✓ Solusi: token yang unik per sesi, dibuat dengan CSPRNG

────────────────────────────────────────────────────────────────────────────

# ✗ Anti-pattern 4: mengandalkan Referer check saja
@app.before_request
def check_referer():
    referer = request.headers.get('Referer', '')
    if 'evil' in referer:  # blacklist approach
        abort(403)
# Blacklist tidak pernah lengkap, attacker bisa hapus Referer header

# ✓ Solusi: whitelist domain yang diizinkan, bukan blacklist yang dilarang

────────────────────────────────────────────────────────────────────────────

# ✗ Anti-pattern 5: mengandalkan Content-Type: application/json saja
# Beberapa browser memungkinkan form untuk mengirim JSON-like content
# Dan beberapa server menerima request tanpa Content-Type check
# Content-Type bukan mekanisme CSRF protection yang reliable

Checklist CSRF Protection #

SEMUA APLIKASI:
  □ Tidak ada state-changing operation melalui GET method
  □ Session cookie menggunakan SameSite=Lax atau Strict
  □ Session cookie menggunakan HttpOnly dan Secure flag

APLIKASI DENGAN FORM HTML:
  □ Semua form yang mengubah data menyertakan CSRF token tersembunyi
  □ Server memvalidasi CSRF token dengan hmac.compare_digest (timing-safe)
  □ CSRF token unik per sesi, dibuat dengan CSPRNG
  □ Token di-regenerasi setelah login

APLIKASI DENGAN AJAX/API:
  □ CSRF token dikirim via header (X-CSRFToken) untuk request modifying
  □ Atau menggunakan Double Submit Cookie pattern
  □ Origin/Referer header divalidasi sebagai lapisan tambahan

SPA:
  □ Jika menggunakan cookie auth: SameSite=Lax minimum + CSRF token untuk operasi sensitif
  □ Jika menggunakan JWT di Authorization header: tidak perlu CSRF token
  □ Interceptor untuk menambahkan CSRF header otomatis sudah dikonfigurasi

OPERASI SANGAT SENSITIF:
  □ Konfirmasi password ulang untuk operasi destruktif (hapus akun)
  □ Rate limiting pada endpoint sensitif
  □ Notifikasi email untuk operasi besar (transfer besar, perubahan email)

Ringkasan #

  • CSRF memanfaatkan kepercayaan server terhadap cookie browser — browser secara otomatis mengirimkan cookie ke domain yang sesuai, termasuk saat request dipicu dari website lain. Attacker tidak perlu tahu password atau mencuri cookie.
  • CORS tidak melindungi dari CSRF — CORS mencegah JavaScript membaca response cross-origin, tapi tidak mencegah request dikirim. Form HTML biasa tidak tunduk pada CORS sama sekali.
  • State-changing operations wajib menggunakan POST/PUT/PATCH/DELETE — GET tidak boleh mengubah data. Ini menghilangkan satu vektor CSRF (img tag, link redirect) sekaligus.
  • Synchronizer Token Pattern adalah perlindungan paling kuat — token random per sesi yang diverifikasi server, tidak bisa ditebak oleh attacker dari domain lain karena Same-Origin Policy.
  • SameSite=Lax adalah lapisan pertahanan CSRF yang paling mudah diterapkan — tidak perlu mengubah logika aplikasi, cukup tambahkan atribut di session cookie. Ini harus menjadi standar minimum.
  • Gunakan hmac.compare_digest untuk membandingkan token — mencegah timing attack di mana attacker menebak token karakter per karakter berdasarkan perbedaan waktu respons.
  • Double Submit Cookie berguna untuk arsitektur stateless — token di cookie dan header harus sama. Attacker tidak bisa membaca nilai cookie dari domain lain untuk dikirim sebagai header.
  • SPA dengan JWT di Authorization header secara natural lebih aman dari CSRF — header custom tidak bisa ditambahkan oleh form HTML biasa. Tapi SPA dengan cookie auth tetap perlu CSRF protection.
  • Operasi destruktif perlu lapisan konfirmasi tambahan — hapus akun, transfer besar, perubahan email/password harus minta konfirmasi password ulang, bukan hanya CSRF token.
  • Defense in depth: kombinasikan SameSite + CSRF token + Origin validation — tidak ada satu mekanisme yang sempurna sendirian. Kombinasi ketiganya memberikan perlindungan yang sangat kuat.

← Sebelumnya: Session Hijacking   Berikutnya: CSRF →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact