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 Session | Penyerang memanfaatkan session korban yang sudah login, bukan mencuri credential |
| Transfer Dana | Memaksa transfer uang dari rekening korban di situs banking |
| Ubah Email/Password | Merubah email atau password akun korban sehingga penyerang bisa mengambil alih |
| Posting Konten | Menulis postingan, komentar, atau review atas nama korban |
| Install Malware | Memaksa korban menginstal malware melalui exploit di router atau IoT device |
| Privilege Escalation | Menaikkan hak akses akun penyerang jika korban adalah admin |
Prasyarat Agar CSRF Bisa Dieksploitasi
- 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
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β 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
<!--
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
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β 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
# β 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
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β 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
# β
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)
// β
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'));
- 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()ataucrypto.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 |
|---|---|---|---|
Strict | Cookie TIDAK dikirim dalam request cross-site sama sekali | Paling aman | Link dari email atau situs lain tidak akan mengirim cookie |
Lax | Cookie dikirim untuk navigasi top-level GET, tapi TIDAK untuk POST/img/iframe | Seimbang | Default browser modern β link di email tetap berfungsi |
None | Cookie dikirim dalam SEMUA request cross-site | Paling lemah | Harus diikuti dengan Secure flag β untuk integrasi lintas domain |
Detail Setiap Nilai SameSite
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β 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
# β
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
// β
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: {} });
});
- 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
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β 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
# β
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?
- HTML
<form>TIDAK bisa mengirim custom header <img>,<script>,<iframe>TIDAK bisa mengirim custom headerXMLHttpRequestdanfetch()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
# β
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 1 | SameSite=Lax/Strict cookies | Mencegah sebagian besar serangan cross-site otomatis |
| Lapisan 2 | CSRF Token (Synchronizer / Double Submit) | Melindungi dari serangan yang lolos SameSite |
| Lapisan 3 | Custom Header Validation | Mencegah form-based attack |
| Lapisan 4 | Re-authentication untuk aksi sensitif | Melindungi operasi kritis (ubah password, hapus akun) |
| Lapisan 5 | CORS Policy yang ketat | Membatasi domain yang bisa berinteraksi dengan API |
Implementasi Lengkap Defense in Depth
# β
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
- β 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
- 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: