☀️Siang
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

Artikel: Web Csrf Protection Artikel: Web Csrf Protection


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

Browser Korban

Cookie: session_id=abc123xyz Domain: bank.examp...

Server bank.example

⚠️ CSRF Problem: Browser mengirim cookie otomat...



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

Diagram

Korban (Already logged in)

Bank App (Target)

Attacker (evil.com)

1. Attacker membuat halaman dengan hidden form/JS
3. Browser auto-submit POST /transfer ke bank.com
4. POST /transfer Cookie: session=abc123
5. Bank App menerima request ✅ Cookie valid (se...

💀 Uang berhasil ditransfer ke attacker!

🛡️ Pertahanan: CSRF Token, SameSite Cookies, Re...

Server-Side: Kode Rentan

Diagram

Browser (User)

Server (Session Store)

Generate CSRF token → simpan di session

🔑 csrf_token: a3f8b2c1...

Verify: token match? session vs body

✅ Token valid → request diproses | ❌ Token sala...

Implementasi di Flask

flowchart TD
    N0["SameSite=Strict ✅ bank.com langsung → cookie DI..."]
    N1["SameSite=Lax (Default) ✅ bank.com langsung → co..."]
    N2["SameSite=None; Secure ✅ SEMUA request → cookie ..."]
    N0 --> N1
    N1 --> N2

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


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
Browser
(JS reads cookie)
Server
1. Set-Cookie: csrf_token=abc123 (httponly=NO)
a) Cookie: csrf_token=abc123
b) Form: <input value="abc123">
2. POST + Cookie header + X-CSRF-Token header
3. Compare both
cookie == header?
✅ Match → process | ❌ Mismatch → 403 Forbidden
💡 Stateless: no server-side session storage needed for token

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