Keamanan

CSRF Protection: Cross-Site Request Forgery

Pelajari cara kerja serangan Cross-Site Request Forgery (CSRF), mengapa cookie-based authentication rentan, dan implementasi berbagai teknik pertahanan seperti Synchronizer Token, Double Submit Cookie, SameSite cookies, serta custom header validation

1. Pengenalan CSRF

Cross-Site Request Forgery (CSRF) β€” juga dikenal sebagai Session Riding atau One-Click Attack β€” adalah jenis serangan keamanan web di mana penyerang memaksa browser korban yang sudah terotentikasi untuk mengirim request HTTP yang tidak diinginkan ke aplikasi web target. Request ini akan dieksekusi dengan hak akses dan session korban, sehingga penyerang dapat melakukan tindakan seolah-olah mereka adalah korban.

CSRF menempati posisi penting dalam daftar OWASP Top 10 dan telah menjadi ancaman serius sejak awal 2000-an. Meskipun banyak framework modern sudah menyertakan perlindungan bawaan, implementasi yang salah atau tidak lengkap masih sering ditemukan di aplikasi web produksi.

Mengapa CSRF Berbahaya?

Aspek Penjelasan
Eksploitasi SessionPenyerang memanfaatkan session korban yang sudah login, bukan mencuri credential
Transfer DanaMemaksa transfer uang dari rekening korban di situs banking
Ubah Email/PasswordMerubah email atau password akun korban sehingga penyerang bisa mengambil alih
Posting KontenMenulis postingan, komentar, atau review atas nama korban
Install MalwareMemaksa korban menginstal malware melalui exploit di router atau IoT device
Privilege EscalationMenaikkan hak akses akun penyerang jika korban adalah admin

Prasyarat Agar CSRF Bisa Dieksploitasi

⚠️ Syarat Serangan CSRF Berhasil
  • Korban sudah terotentikasi β€” memiliki session aktif di situs target
  • Situs menggunakan cookie untuk autentikasi β€” browser mengirim cookie otomatis
  • Tidak ada validasi asal request β€” server tidak memeriksa dari mana request berasal
  • Ada aksi yang bisa dieksploitasi β€” perubahan data, transfer uang, ubah setting
Diagram: Mengapa Cookie-Based Auth Rentan terhadap CSRF
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚            COOKIE-BASED AUTHENTICATION & CSRF              β”‚
β”‚                                                            β”‚
β”‚  Browser korban (sudah login):                             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                  β”‚
β”‚  β”‚ Cookie: session_id=abc123xyz         β”‚                  β”‚
β”‚  β”‚ Domain: bank.example.com             β”‚                  β”‚
β”‚  β”‚ HttpOnly: true                       β”‚                  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                  β”‚
β”‚                                                            β”‚
β”‚  Saat browser mengakses bank.example.com:                  β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    Request         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”‚
β”‚  β”‚  Browser │───────────────────▢│  Server      β”‚         β”‚
β”‚  β”‚  korban  β”‚ + Cookie otomatis  β”‚  bank.exampleβ”‚         β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β”‚
β”‚                                                            β”‚
β”‚  Masalah: browser mengirim cookie ke domain yang sama      β”‚
β”‚  TANPA peduli dari halaman mana request berasal!           β”‚
β”‚  Penyerang bisa memicu request dari situs manapun.         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

2. Cara Kerja Serangan CSRF

Serangan CSRF biasanya dimulai dengan penyerang membuat halaman web berbahaya yang berisi request tersembunyi ke situs target. Ketika korban mengunjungi halaman tersebut, browser korban secara otomatis mengirim request beserta semua cookie yang relevan ke situs target.

Skenario Serangan: Transfer Uang

HTML β€” Payload Serangan CSRF
<!--
  Halaman ini dibuat oleh penyerang.
  Korban yang sudah login di bank.example.com
  mengunjungi halaman ini secara tidak sengaja.
-->

<!DOCTYPE html>
<html>
<head>
  <title>Anda Memenangkan Hadiah!</title>
</head>
<body>
  <h1>Selamat! Anda memenangkan iPhone!</h1>

  <!-- Skenario 1: CSRF via Form (POST) -->
  <!-- Form ini otomatis di-submit menggunakan JavaScript -->
  <form id="csrf-form" action="https://bank.example.com/transfer" method="POST">
    <input type="hidden" name="to_account" value="ATTACKER_ACCOUNT">
    <input type="hidden" name="amount" value="10000000">
    <input type="hidden" name="currency" value="IDR">
  </form>
  <script>
    // Form otomatis di-submit begitu halaman dimuat
    document.getElementById('csrf-form').submit();
  </script>

  <!-- Skenario 2: CSRF via Image Tag (GET) -->
  <!-- Beberapa aplikasi rentan karena menggunakan GET untuk aksi -->
  <img src="https://bank.example.com/transfer?to=ATTACKER&amount=10000000"
       style="display:none" alt="">

  <!-- Skenario 3: CSRF via XMLHttpRequest (XHR) -->
  <script>
    var xhr = new XMLHttpRequest();
    xhr.open('POST', 'https://bank.example.com/transfer', true);
    xhr.withCredentials = true; // Kirim cookie
    xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
    xhr.send('to_account=ATTACKER&amount=10000000');
  </script>

  <!-- Skenario 4: CSRF via Fetch API -->
  <script>
    fetch('https://bank.example.com/transfer', {
      method: 'POST',
      credentials: 'include', // Kirim cookie
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        to_account: 'ATTACKER_ACCOUNT',
        amount: 10000000
      })
    });
  </script>
</body>
</html>

Alur Serangan CSRF Langkah Demi Langkah

Diagram: Alur Serangan CSRF
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              CSRF ATTACK FLOW                               β”‚
β”‚                                                            β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   1. Login          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”        β”‚
β”‚  β”‚  Korban  │────────────────────▢│  Bank App    β”‚        β”‚
β”‚  β”‚          │◀──session cookie────│  (Target)    β”‚        β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜        β”‚
β”‚       β”‚                                                    β”‚
β”‚       β”‚ 2. Kunjungi situs jahat                           β”‚
β”‚       β–Ό                                                    β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   3. Halaman jahat  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”        β”‚
β”‚  β”‚  Halaman β”‚   memuat form/JS    β”‚  Attacker    β”‚        β”‚
β”‚  β”‚  Jahat   β”‚   yang auto-submit  β”‚  Server      β”‚        β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜        β”‚
β”‚       β”‚                                                    β”‚
β”‚       β”‚ 4. Browser otomatis mengirim request               β”‚
β”‚       β”‚    + cookie bank.example.com                       β”‚
β”‚       β–Ό                                                    β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                  β”‚
β”‚  β”‚  POST /transfer                      β”‚                  β”‚
β”‚  β”‚  Cookie: session=abc123              β”‚                  β”‚
β”‚  β”‚  Body: to=attacker&amount=10M        β”‚                  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                  β”‚
β”‚                 β”‚                                          β”‚
β”‚                 β–Ό                                          β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                  β”‚
β”‚  β”‚  Bank App Menerima Request           β”‚                  β”‚
β”‚  β”‚  βœ… Cookie valid (session korban)    β”‚                  β”‚
β”‚  β”‚  ❌ Tidak tahu request dari mana     β”‚                  β”‚
β”‚  β”‚  β†’ Transfer DIEKSEKUSI!              β”‚                  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Server-Side: Kode Rentan

Python Flask β€” Endpoint Transfer Rentan CSRF
# ❌ RENTAN: Endpoint tanpa perlindungan CSRF
from flask import Flask, request, session, jsonify

app = Flask(__name__)
app.secret_key = 'rahasia-sangat-rahasia'

@app.route('/login', methods=['POST'])
def login():
    username = request.form.get('username')
    password = request.form.get('password')
    if authenticate(username, password):
        session['user_id'] = get_user_id(username)
        session['username'] = username
        return jsonify({"message": "Login berhasil"})
    return jsonify({"error": "Gagal login"}), 401

# ❌ TIDAK ADA CSRF PROTECTION!
@app.route('/transfer', methods=['POST'])
def transfer():
    if 'user_id' not in session:
        return jsonify({"error": "Unauthorized"}), 401

    to_account = request.form.get('to_account')
    amount = int(request.form.get('amount', 0))

    # Langsung eksekusi tanpa validasi asal request
    execute_transfer(session['user_id'], to_account, amount)
    return jsonify({"message": f"Transfer Rp{amount:,} berhasil ke {to_account}"})

3. Synchronizer Token Pattern

Synchronizer Token Pattern adalah teknik pertahanan CSRF yang paling umum dan direkomendasikan. Server menghasilkan token unik yang tidak dapat diprediksi dan menyimpannya di session pengguna. Token ini kemudian disisipkan ke dalam setiap form HTML yang membutuhkan proteksi. Saat form disubmit, server memverifikasi bahwa token yang dikirim cocok dengan token yang tersimpan di session.

Cara Kerja Synchronizer Token

Diagram: Synchronizer Token Pattern
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚          SYNCHRONIZER TOKEN PATTERN                        β”‚
β”‚                                                            β”‚
β”‚  1. User Request Form:                                     β”‚
β”‚  Browser ──GET /transfer-form──▢ Server                    β”‚
β”‚                                                            β”‚
β”‚  2. Server Generate Token:                                 β”‚
β”‚  Server: token = crypto.random(32)                         β”‚
β”‚          session['csrf_token'] = token                     β”‚
β”‚                                                            β”‚
β”‚  3. Response with Hidden Field:                            β”‚
β”‚  Server ──▢ HTML dengan <input type="hidden"               β”‚
β”‚              name="csrf_token" value="abc123xyz...">       β”‚
β”‚                                                            β”‚
β”‚  4. User Submit Form:                                      β”‚
β”‚  Browser ──POST /transfer──▢ Server                        β”‚
β”‚  + cookie(session) + form(csrf_token)                      β”‚
β”‚                                                            β”‚
β”‚  5. Server Verify:                                         β”‚
β”‚  if form['csrf_token'] == session['csrf_token']:           β”‚
β”‚      β†’ Request VALID, eksekusi                             β”‚
β”‚  else:                                                     β”‚
β”‚      β†’ Request INVALID (mungkin CSRF), tolak!              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Implementasi di Flask

Python Flask β€” Implementasi CSRF Token
# βœ… AMAN: Implementasi Synchronizer Token
from flask import Flask, request, session, abort, render_template_string
import secrets
import hmac

app = Flask(__name__)
app.secret_key = secrets.token_hex(32)

def generate_csrf_token():
    """Generate atau ambil CSRF token dari session"""
    if '_csrf_token' not in session:
        session['_csrf_token'] = secrets.token_hex(32)
    return session['_csrf_token']

def validate_csrf_token(token):
    """Validasi CSRF token menggunakan constant-time comparison"""
    session_token = session.get('_csrf_token')
    if not session_token or not token:
        return False
    # Gunakan hmac.compare_digest untuk mencegah timing attack
    return hmac.compare_digest(session_token, token)

# Inject token ke semua template
@app.context_processor
def inject_csrf_token():
    return dict(csrf_token=generate_csrf_token)

# Middleware untuk validasi CSRF pada semua POST request
@app.before_request
def csrf_protect():
    if request.method == 'POST':
        # Cek token dari form field atau header
        token = request.form.get('csrf_token') or request.headers.get('X-CSRF-Token')
        if not validate_csrf_token(token):
            abort(403, "CSRF token tidak valid atau tidak ada")

# Template HTML dengan CSRF token
TRANSFER_FORM = '''
<form method="POST" action="/transfer">
    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
    <label>Rekening Tujuan:
        <input type="text" name="to_account" required>
    </label>
    <label>Jumlah (IDR):
        <input type="number" name="amount" min="10000" required>
    </label>
    <button type="submit">Transfer</button>
</form>
'''

@app.route('/transfer', methods=['GET'])
def transfer_form():
    return render_template_string(TRANSFER_FORM)

@app.route('/transfer', methods=['POST'])
def transfer():
    if 'user_id' not in session:
        abort(401)
    to_account = request.form.get('to_account')
    amount = int(request.form.get('amount', 0))
    execute_transfer(session['user_id'], to_account, amount)
    return f"Transfer Rp{amount:,} berhasil!"

Implementasi di Express.js (Node.js)

JavaScript Express β€” CSRF Token dengan csurf
// βœ… AMAN: Implementasi CSRF di Express.js
const express = require('express');
const session = require('express-session');
const crypto = require('crypto');
const app = express();

app.use(session({
  secret: crypto.randomBytes(64).toString('hex'),
  resave: false,
  saveUninitialized: true,
  cookie: {
    secure: true,      // Hanya HTTPS
    httpOnly: true,     // Tidak bisa diakses JS
    sameSite: 'strict'  // SameSite strict
  }
}));

// Middleware CSRF custom
function csrfProtection(req, res, next) {
  if (req.method === 'GET') {
    // Generate token untuk form
    if (!req.session.csrfToken) {
      req.session.csrfToken = crypto.randomBytes(32).toString('hex');
    }
    next();
  } else if (req.method === 'POST') {
    const token = req.body._csrf || req.headers['x-csrf-token'];
    if (!token || token !== req.session.csrfToken) {
      return res.status(403).json({
        error: 'CSRF token tidak valid'
      });
    }
    // Regenerate token setelah setiap POST (one-time token)
    req.session.csrfToken = crypto.randomBytes(32).toString('hex');
    next();
  }
}

app.use(csrfProtection);

// Template engine (EJS)
app.get('/transfer', (req, res) => {
  res.send(`
    <form method="POST" action="/transfer">
      <input type="hidden" name="_csrf" value="${req.session.csrfToken}">
      <input type="text" name="to_account" placeholder="Rekening Tujuan">
      <input type="number" name="amount" placeholder="Jumlah">
      <button type="submit">Transfer</button>
    </form>
  `);
});

app.post('/transfer', (req, res) => {
  const { to_account, amount } = req.body;
  // Proses transfer...
  res.json({ message: `Transfer Rp${amount} berhasil ke ${to_account}` });
});

app.listen(3000, () => console.log('Server berjalan di port 3000'));
πŸ’‘ Perbedaan Synchronizer Token vs Random CSRF Token
  • Synchronizer Token: Token disimpan di session server, diverifikasi setiap request POST
  • One-Time Token: Token di-regenerate setelah setiap penggunaan β€” lebih aman tapi bisa bermasalah dengan back button
  • Per-Request Token: Token baru untuk setiap halaman, cocok untuk SPA
  • Token harus cryptographically random β€” gunakan secrets.token_hex() atau crypto.randomBytes()
  • JANGAN gunakan token yang bisa diprediksi seperti timestamp atau user ID

4. SameSite Cookies

SameSite adalah atribut pada cookie yang menentukan apakah cookie boleh dikirim dalam cross-site request. Ini adalah pertahanan browser-level terhadap CSRF yang sangat efektif dan memerlukan sedikit atau tanpa perubahan kode di sisi server. Atribut ini didukung oleh semua browser modern.

Nilai SameSite Cookie

Nilai Perilaku Keamanan Contoh Kasus
StrictCookie TIDAK dikirim dalam request cross-site sama sekaliPaling amanLink dari email atau situs lain tidak akan mengirim cookie
LaxCookie dikirim untuk navigasi top-level GET, tapi TIDAK untuk POST/img/iframeSeimbangDefault browser modern β€” link di email tetap berfungsi
NoneCookie dikirim dalam SEMUA request cross-sitePaling lemahHarus diikuti dengan Secure flag β€” untuk integrasi lintas domain

Detail Setiap Nilai SameSite

Diagram: SameSite Cookie Behavior
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚            SAMESITE COOKIE BEHAVIOR                         β”‚
β”‚                                                            β”‚
β”‚  Situs A (bank.com) ──▢ Cookie: session=abc               β”‚
β”‚                                                            β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚ SameSite=Strict:                                    β”‚   β”‚
β”‚  β”‚                                                     β”‚   β”‚
β”‚  β”‚ βœ… bank.com langsung β†’ cookie DIKIRIM              β”‚   β”‚
β”‚  β”‚ ❌ evil.com β†’ bank.com β†’ cookie TIDAK dikirim      β”‚   β”‚
β”‚  β”‚ ❌ email link ke bank.com β†’ cookie TIDAK dikirim    β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚                                                            β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚ SameSite=Lax (default browser modern):              β”‚   β”‚
β”‚  β”‚                                                     β”‚   β”‚
β”‚  β”‚ βœ… bank.com langsung β†’ cookie DIKIRIM              β”‚   β”‚
β”‚  β”‚ ❌ evil.com POST ke bank.com β†’ cookie TIDAK dikirim β”‚   β”‚
β”‚  β”‚ βœ… Link di email (GET top-level) β†’ cookie DIKIRIM  β”‚   β”‚
β”‚  β”‚ ❌ evil.com <img src="bank.com"> β†’ TIDAK dikirim   β”‚   β”‚
β”‚  β”‚ ❌ evil.com <iframe src="bank.com"> β†’ TIDAK dikirimβ”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚                                                            β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚ SameSite=None; Secure:                              β”‚   β”‚
β”‚  β”‚                                                     β”‚   β”‚
β”‚  β”‚ βœ… SEMUA request β†’ cookie SELALU dikirim            β”‚   β”‚
β”‚  β”‚ ⚠️ Hanya bekerja di HTTPS (Secure wajib)           β”‚   β”‚
β”‚  β”‚ ⚠️ Sama sekali TIDAK melindungi dari CSRF          β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Implementasi SameSite Cookies

Python Flask β€” Set SameSite Cookie
# βœ… Implementasi SameSite Cookies di Flask
from flask import Flask, session, make_response
import secrets

app = Flask(__name__)
app.secret_key = secrets.token_hex(32)

# Konfigurasi global untuk session cookies
app.config.update(
    SESSION_COOKIE_SECURE=True,        # Hanya kirim via HTTPS
    SESSION_COOKIE_HTTPONLY=True,       # Tidak bisa diakses JavaScript
    SESSION_COOKIE_SAMESITE='Lax',     # SameSite=Lax (default yang aman)
    SESSION_COOKIE_NAME='__Host-session',  # Prefix __Host untuk keamanan
    PERMANENT_SESSION_LIFETIME=3600    # 1 jam timeout
)

# Jika butuh SameSite=Strict untuk endpoint sensitif
@app.route('/change-password', methods=['POST'])
def change_password():
    if 'user_id' not in session:
        return {"error": "Unauthorized"}, 401

    # Proses ganti password...
    resp = make_response({"message": "Password berhasil diubah"})

    # Set cookie dengan SameSite=Strict untuk keamanan maksimal
    resp.set_cookie(
        'password_changed',
        'true',
        secure=True,
        httponly=True,
        samesite='Strict',
        max_age=300  # 5 menit
    )
    return resp

# Jika butuh SameSite=None untuk cross-origin API calls
@app.route('/api/widget-data', methods=['GET'])
def widget_data():
    resp = make_response({"data": [1, 2, 3]})
    resp.set_cookie(
        'widget_session',
        'xyz789',
        secure=True,         # WAJIB untuk SameSite=None
        httponly=True,
        samesite='None',     # Izinkan cross-site
        max_age=86400        # 1 hari
    )
    return resp
JavaScript Express β€” SameSite Cookies
// βœ… Implementasi SameSite Cookies di Express.js
const express = require('express');
const session = require('express-session');
const app = express();

// Session dengan SameSite=Lax
app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  name: '__Host-session',
  cookie: {
    secure: true,         // Hanya HTTPS
    httpOnly: true,       // Tidak bisa diakses JS
    sameSite: 'lax',      // SameSite=Lax
    maxAge: 3600000       // 1 jam (dalam milidetik)
  }
}));

// Set cookie manual dengan SameSite=Strict
app.post('/api/admin/delete-user', (req, res) => {
  // Endpoint sensitif β†’ SameSite=Strict
  res.cookie('admin_action', 'delete_user', {
    secure: true,
    httpOnly: true,
    sameSite: 'strict',
    maxAge: 60000
  });

  // Proses hapus user...
  res.json({ message: 'User berhasil dihapus' });
});

// Cookie untuk widget lintas domain
app.get('/api/embed/widget', (req, res) => {
  res.cookie('embed_token', 'token123', {
    secure: true,         // WAJIB untuk SameSite=None
    httpOnly: true,
    sameSite: 'none',     // Izinkan cross-site
    maxAge: 86400000
  });
  res.json({ widgetData: {} });
});
⚠️ Catatan Penting SameSite Cookies
  • SameSite=Lax adalah default di Chrome, Edge, dan Firefox sejak 2020 β€” Anda tidak perlu men-setting secara eksplisit untuk perilaku default
  • SameSite=None memerlukan Secure flag β€” cookie hanya akan dikirim via HTTPS
  • SameSite=Strict bisa memutus UX β€” ketika user klik link dari email, mereka tidak akan langsung login karena cookie tidak dikirim
  • Lax memberikan perlindungan yang cukup untuk sebagian besar kasus β€” mencegah POST-based CSRF
  • SameSite saja TIDAK cukup β€” tetap gunakan CSRF token sebagai lapisan pertahanan tambahan

5. Double Submit Cookie Pattern

Double Submit Cookie adalah teknik pertahanan CSRF di mana token yang sama dikirim dalam dua tempat: sebagai cookie dan sebagai parameter request (form field atau header). Server kemudian memverifikasi bahwa kedua token tersebut cocok. Keuntungan utama pola ini adalah tidak memerlukan penyimpanan state di server, sehingga sangat cocok untuk arsitektur stateless dan microservices.

Cara Kerja Double Submit Cookie

Diagram: Double Submit Cookie Pattern
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚          DOUBLE SUBMIT COOKIE PATTERN                      β”‚
β”‚                                                            β”‚
β”‚  1. Client-Side Generate Token:                            β”‚
β”‚  JavaScript: token = crypto.randomUUID()                   β”‚
β”‚                                                            β”‚
β”‚  2. Simpan Token di Dua Tempat:                            β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”            β”‚
β”‚  β”‚ a) Cookie: csrf_token=abc123 (httponly=NO) β”‚            β”‚
β”‚  β”‚ b) Form: <input value="abc123">            β”‚            β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜            β”‚
β”‚                                                            β”‚
β”‚  3. Saat Submit:                                           β”‚
β”‚  Browser ──POST──▢ Server                                  β”‚
β”‚  Cookie: csrf_token=abc123                                 β”‚
β”‚  Form:   csrf_token=abc123                                 β”‚
β”‚                                                            β”‚
β”‚  4. Server Verifikasi:                                     β”‚
β”‚  if cookie['csrf_token'] == form['csrf_token']:            β”‚
β”‚      β†’ Valid! (penyerang tidak bisa baca/tulis cookie)     β”‚
β”‚  else:                                                     β”‚
β”‚      β†’ Invalid! Tolak request                              β”‚
β”‚                                                            β”‚
β”‚  Mengapa Aman?                                             β”‚
β”‚  - Penyerang BISA menulis cookie (via CSRF form)           β”‚
β”‚  - Tapi penyerang TIDAK BISA membaca cookie korban         β”‚
β”‚  - Jadi penyerang tidak bisa mengirim token yang cocok     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Implementasi Double Submit Cookie

Python Flask β€” Double Submit Cookie
# βœ… Double Submit Cookie Pattern β€” Stateless CSRF Protection
from flask import Flask, request, make_response, jsonify, abort
import secrets
import hmac

app = Flask(__name__)
app.secret_key = secrets.token_hex(32)

@app.route('/api/csrf-token', methods=['GET'])
def get_csrf_token():
    """Endpoint untuk mendapatkan CSRF token"""
    # Generate token cryptographically random
    csrf_token = secrets.token_hex(32)

    resp = make_response(jsonify({"csrf_token": csrf_token}))

    # Simpan di cookie (TIDAK httponly agar JS bisa baca)
    resp.set_cookie(
        'csrf_token',
        csrf_token,
        secure=True,
        httponly=False,   # Agar JavaScript bisa baca untuk header
        samesite='Lax',
        max_age=3600
    )
    return resp

@app.before_request
def validate_double_submit():
    """Validasi Double Submit Cookie pada semua state-changing requests"""
    if request.method in ('POST', 'PUT', 'DELETE', 'PATCH'):
        # Ambil token dari cookie
        cookie_token = request.cookies.get('csrf_token')

        # Ambil token dari header atau form
        header_token = request.headers.get('X-CSRF-Token')
        form_token = request.form.get('csrf_token')
        request_token = header_token or form_token

        # Verifikasi keduanya ada dan cocok
        if not cookie_token or not request_token:
            abort(403, "CSRF token tidak lengkap")

        if not hmac.compare_digest(cookie_token, request_token):
            abort(403, "CSRF token tidak cocok")

# Contoh endpoint yang dilindungi
@app.route('/api/transfer', methods=['POST'])
def transfer():
    data = request.get_json()
    to_account = data.get('to_account')
    amount = data.get('amount')
    # Proses transfer...
    return jsonify({"message": f"Transfer Rp{amount:,} berhasil"})

# Client-side JavaScript untuk menggunakan token
@app.route('/transfer')
def transfer_page():
    return '''
    <html>
    <body>
      <form id="transferForm">
        <input type="text" name="to_account" placeholder="Rekening Tujuan">
        <input type="number" name="amount" placeholder="Jumlah">
        <button type="submit">Transfer</button>
      </form>
      <script>
        // Ambil CSRF token dari cookie
        function getCookie(name) {
          const value = `; ${document.cookie}`;
          const parts = value.split(`; ${name}=`);
          if (parts.length === 2) return parts.pop().split(';').shift();
        }

        // Kirim dengan CSRF token di header
        document.getElementById('transferForm').addEventListener('submit',
          async (e) => {
            e.preventDefault();
            const formData = new FormData(e.target);
            const csrfToken = getCookie('csrf_token');

            const response = await fetch('/api/transfer', {
              method: 'POST',
              headers: {
                'Content-Type': 'application/json',
                'X-CSRF-Token': csrfToken  // Token di header
              },
              body: JSON.stringify(Object.fromEntries(formData))
            });
            const result = await response.json();
            alert(result.message);
          }
        );
      </script>
    </body>
    </html>
    '''

6. Custom Header Validation

Teknik Custom Header Validation memanfaatkan fakta bahwa penyerang tidak dapat mengirim custom HTTP header dalam cross-origin request dari browser. Dengan memeriksa keberadaan header kustom tertentu (seperti X-Requested-With), server dapat memverifikasi bahwa request berasal dari JavaScript asli di situs sendiri, bukan dari form CSRF.

Mengapa Custom Header Aman?

πŸ“‹ Proteksi Custom Header
  • HTML <form> TIDAK bisa mengirim custom header
  • <img>, <script>, <iframe> TIDAK bisa mengirim custom header
  • XMLHttpRequest dan fetch() bisa mengirim custom header, TAPI akan memicu CORS preflight OPTIONS request
  • Jika server tidak mengizinkan CORS, browser akan memblokir request
  • Jadi: keberadaan custom header = request dari JavaScript situs sendiri
Python β€” Custom Header CSRF Validation
# βœ… Custom Header Validation
from flask import Flask, request, abort, jsonify

app = Flask(__name__)

@app.before_request
def check_custom_header():
    """Validasi custom header pada semua state-changing requests"""
    if request.method in ('POST', 'PUT', 'DELETE', 'PATCH'):
        # Cek keberadaan custom header
        requested_with = request.headers.get('X-Requested-With')

        if requested_with != 'XMLHttpRequest':
            abort(403, "Request harus mengandung X-Requested-With header")

        # Opsional: cek Content-Type untuk POST
        if request.method == 'POST':
            content_type = request.content_type or ''
            allowed_types = [
                'application/json',
                'application/x-www-form-urlencoded',
                'multipart/form-data'
            ]
            if not any(ct in content_type for ct in allowed_types):
                abort(415, "Content-Type tidak didukung")

@app.route('/api/transfer', methods=['POST'])
def transfer():
    data = request.get_json()
    return jsonify({"message": "Transfer berhasil", "data": data})

# Client-side: Tambahkan custom header ke semua fetch request
@app.route('/app')
def app_page():
    return '''
    <script>
    // Intercept semua fetch request dan tambahkan custom header
    const originalFetch = window.fetch;
    window.fetch = function(url, options = {}) {
      options.headers = options.headers || {};

      // Tambahkan custom header untuk semua non-GET request
      if (options.method && options.method.toUpperCase() !== 'GET') {
        options.headers['X-Requested-With'] = 'XMLHttpRequest';
      }

      // Pastikan credentials dikirim
      options.credentials = 'same-origin';

      return originalFetch.call(this, url, options);
    };

    // Atau gunakan Axios interceptor
    // import axios from 'axios';
    // axios.interceptors.request.use(config => {
    //   if (config.method !== 'get') {
    //     config.headers['X-Requested-With'] = 'XMLHttpRequest';
    //   }
    //   return config;
    // });
    </script>
    '''

7. Best Practices & Defense in Depth

Pendekatan terbaik untuk melindungi aplikasi dari CSRF adalah menggunakan Defense in Depth β€” kombinasi beberapa teknik pertahanan sekaligus. Tidak ada satu solusi tunggal yang sempurna, jadi lapisan pertahanan berganda memastikan bahwa jika satu lapisan gagal, lapisan lainnya tetap melindungi.

Strategi Defense in Depth

Lapisan Teknik Kekuatan
Lapisan 1SameSite=Lax/Strict cookiesMencegah sebagian besar serangan cross-site otomatis
Lapisan 2CSRF Token (Synchronizer / Double Submit)Melindungi dari serangan yang lolos SameSite
Lapisan 3Custom Header ValidationMencegah form-based attack
Lapisan 4Re-authentication untuk aksi sensitifMelindungi operasi kritis (ubah password, hapus akun)
Lapisan 5CORS Policy yang ketatMembatasi domain yang bisa berinteraksi dengan API

Implementasi Lengkap Defense in Depth

Python Flask β€” CSRF Protection Lengkap
# βœ… Defense in Depth: Kombinasi Semua Teknik CSRF
from flask import Flask, request, session, abort, jsonify
from flask_cors import CORS
from functools import wraps
import secrets
import hmac
import time

app = Flask(__name__)
app.secret_key = secrets.token_hex(32)

# CORS yang ketat β€” hanya izinkan origin tertentu
CORS(app, origins=['https://beebanelabs.pages.dev'], supports_credentials=True)

# Konfigurasi Cookie
app.config.update(
    SESSION_COOKIE_SECURE=True,
    SESSION_COOKIE_HTTPONLY=True,
    SESSION_COOKIE_SAMESITE='Lax',
    SESSION_COOKIE_NAME='__Host-session',
    PERMANENT_SESSION_LIFETIME=1800
)

# === LAPISAN 1: SameSite Cookies (sudah di-set di atas) ===

# === LAPISAN 2: CSRF Token ===
def generate_csrf_token():
    if '_csrf_token' not in session:
        session['_csrf_token'] = secrets.token_hex(32)
    return session['_csrf_token']

def validate_csrf_token(token):
    session_token = session.get('_csrf_token')
    if not session_token or not token:
        return False
    return hmac.compare_digest(session_token, token)

# === LAPISAN 3: Custom Header Validation ===
def check_custom_header():
    xrw = request.headers.get('X-Requested-With')
    return xrw == 'XMLHttpRequest'

# === LAPISAN 4: Re-authentication untuk aksi sensitif ===
def require_reauth(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        password = request.headers.get('X-Confirm-Password')
        if not password:
            abort(403, "Password confirmation required")
        user = get_user(session['user_id'])
        if not verify_password(password, user['password_hash']):
            abort(403, "Password incorrect")
        return f(*args, **kwargs)
    return decorated

# Middleware gabungan
@app.before_request
def csrf_protection():
    if request.method in ('POST', 'PUT', 'DELETE', 'PATCH'):
        # Lapisan 2: CSRF Token
        token = (request.headers.get('X-CSRF-Token') or
                 request.form.get('csrf_token') or
                 (request.get_json(silent=True) or {}).get('csrf_token'))

        if not validate_csrf_token(token):
            abort(403, "CSRF token invalid")

        # Lapisan 3: Custom Header (opsional, tambahan)
        if not check_custom_header():
            abort(403, "Missing X-Requested-With header")

@app.context_processor
def inject_csrf():
    return dict(csrf_token=generate_csrf_token)

# === LAPISAN 5: CORS (sudah di-set di atas) ===

# Endpoint reguler
@app.route('/api/profile', methods=['PUT'])
def update_profile():
    if 'user_id' not in session:
        abort(401)
    data = request.get_json()
    update_user_profile(session['user_id'], data)
    return jsonify({"message": "Profil berhasil diperbarui"})

# Endpoint sensitif dengan re-authentication
@app.route('/api/delete-account', methods=['DELETE'])
@require_reauth
def delete_account():
    if 'user_id' not in session:
        abort(401)
    delete_user(session['user_id'])
    session.clear()
    return jsonify({"message": "Akun berhasil dihapus"})

Checklist CSRF Protection

πŸ’‘ CSRF Protection Checklist
  • βœ… SameSite cookies β€” setel Lax atau Strict untuk semua session cookies
  • βœ… CSRF token β€” implementasikan di semua form dan state-changing API endpoints
  • βœ… Custom headers β€” validasi X-Requested-With atau header kustom lainnya
  • βœ… Re-authentication β€” minta password ulang untuk operasi sensitif
  • βœ… CORS yang ketat β€” jangan izinkan origin wildcard dengan credentials
  • βœ… GET harus idempotent β€” jangan lakukan aksi perubahan data via GET
  • βœ… Constant-time comparison β€” gunakan hmac.compare_digest()
  • βœ… Token regeneration β€” regenerate token setelah login dan untuk aksi penting
  • βœ… HTTPS everywhere β€” cookie Secure flag dan HSTS header
  • βœ… Security headers β€” X-Frame-Options, Content-Security-Policy

Teknik CSRF yang TIDAK Direkomendasikan

⚠️ Hindari Teknik Ini
  • Mengandalkan Referer/Origin header saja β€” bisa di-strip oleh proxy atau privacy extension
  • GET request untuk aksi β€” mudah dieksploitasi via <img> tag
  • CSRF token yang bisa diprediksi β€” gunakan cryptographically random
  • Hanya mengandalkan SameSite=None β€” tidak memberikan perlindungan apapun
  • Menyimpan CSRF token di localStorage β€” rentan terhadap XSS
  • Menggunakan session ID sebagai CSRF token β€” bocor melalui log dan referer

8. Quiz: Uji Pemahamanmu!

Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut untuk menguji pemahamanmu tentang CSRF Protection:

Pertanyaan 1: Apa prasyarat utama agar serangan CSRF berhasil?

a) Korban sudah terotentikasi dan browser mengirim cookie otomatis
b) Korban menggunakan password yang lemah
c) Server menggunakan HTTP biasa (tanpa HTTPS)
d) Korban menginstal aplikasi penyerang

Pertanyaan 2: SameSite cookie dengan nilai apa yang memberikan keseimbangan terbaik antara keamanan dan UX?

a) SameSite=None
b) SameSite=Lax
c) SameSite=Strict
d) Tanpa SameSite

Pertanyaan 3: Apa keuntungan utama Double Submit Cookie dibanding Synchronizer Token?

a) Lebih mudah diimplementasikan
b) Tidak memerlukan penyimpanan state di server (stateless)
c) Bekerja tanpa HTTPS
d) Tidak memerlukan cookie sama sekali

Pertanyaan 4: Mengapa custom header seperti X-Requested-With efektif melawan CSRF?

a) Karena custom header dienkripsi
b) Karena HTML form dan tag seperti img/iframe tidak bisa mengirim custom header
c) Karena custom header hanya bisa dikirim di HTTPS
d) Karena browser memblokir semua request dengan custom header

Pertanyaan 5: Fungsi apa yang harus digunakan untuk membandingkan CSRF token agar aman dari timing attack?

a) operator == biasa
b) str.equals()
c) hmac.compare_digest()
d) string.startswith()
πŸ” Zoom
100%
🎨 Tema