CSRF

Token CSRF dan atribut cookie SameSite.

Daftar isi

CSRF (cross-site request forgery) adalah serangan yang memaksa browser pengguna mengirim permintaan palsu ke aplikasi dengan memanfaatkan kredensial yang terkirim secara otomatis. Topik ini membahas alur serangannya, pertahanan berupa token CSRF yang disimpan di session dan diverifikasi dengan hash_equals, serta atribut cookie SameSite. Setelah membacanya, setiap aksi yang mengubah data dapat dilengkapi pertahanan yang menuntut kehadiran token.

Cara kerja serangan CSRF#

Browser melampirkan cookie pada setiap permintaan yang ditujukan ke domain penerbitnya, termasuk permintaan yang dipicu oleh halaman lain. Perilaku inilah yang dimanfaatkan serangan CSRF. Sebuah halaman jahat dapat memuat form tersembunyi yang otomatis mengirim permintaan POST ke aplikasi target, misalnya menyimpan berita atas nama pengguna yang sedang login. Aplikasi menerima permintaan dengan cookie session yang sah, sehingga tidak memiliki cara membedakan klik sungguhan dari permintaan palsu bila hanya bergantung pada cookie.

Sasaran serangan ini adalah aksi yang mengubah data, seperti menyimpan, menghapus, atau mengganti kata sandi. Permintaan palsu berjalan tanpa sepengetahuan pengguna, dan aplikasi yang hanya mengandalkan cookie akan mengeksekusinya seolah-olah sah. Diagram berikut menggambarkan alurnya dari halaman jahat sampai server yang menolak permintaan tanpa token.

Halaman jahatform tersembunyiBrowser korbancookie ikut terkirimAplikasi targetPOST tanpa tokenDitolak403
Alur serangan CSRF dan letak pencegahannya

Token CSRF pada form#

Pertahanan utamanya adalah token rahasia yang tersimpan di session server dan dibawa oleh setiap form. Server menghasilkan token acak 32 byte dengan random_bytes lalu menyimpannya di $_SESSION, dan form membawanya sebagai input tersembunyi. Saat permintaan masuk, server membandingkan nilai kiriman dengan nilai session menggunakan hash_equals. Fungsi itu adalah perbandingan string yang durasi eksekusinya tidak bergantung pada isinya, sehingga kebal terhadap serangan pengukuran waktu.

php
<?php
session_start();

$_SESSION['token_csrf'] = bin2hex(random_bytes(32));
$tokenSesi = $_SESSION['token_csrf'];

$tokenDikirim = $tokenSesi;
var_dump(hash_equals($tokenSesi, $tokenDikirim));

$tokenPenyerang = 'a1b2c3d4';
var_dump(hash_equals($tokenSesi, $tokenPenyerang));

echo 'Panjang token: ' . strlen($tokenSesi) . PHP_EOL;

session_write_close();
text Keluaran
bool(true)
bool(false)
Panjang token: 64

Keluaran menunjukkan token sah yang dikirim kembali lolos, sedangkan teks karangan dari luar ditolak. Panjang token 64 karakter berasal dari 32 byte acak yang diserialkan bin2hex, dan nilainya tidak perlu dicetak untuk melakukan verifikasi. Berikut adalah berkas form yang menyatukan seluruh langkah, dijalankan melalui CLI untuk memeriksa keluarannya.

php form-berita.php
<?php
if (PHP_SAPI === 'cli') {
    echo 'Berkas ini dirancang untuk berjalan melalui server web.' . PHP_EOL;
    exit;
}

session_start();

if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') {
    $tokenKirim = $_POST['token'] ?? '';
    $tokenSesi  = $_SESSION['token_csrf'] ?? '';

    if (!is_string($tokenKirim) || $tokenKirim === '' || !hash_equals($tokenSesi, $tokenKirim)) {
        http_response_code(403);
        exit('Permintaan ditolak: token CSRF tidak valid.' . PHP_EOL);
    }

    session_regenerate_id(true);
    unset($_SESSION['token_csrf']);
    exit('Berita tersimpan.' . PHP_EOL);
}

if (!isset($_SESSION['token_csrf'])) {
    $_SESSION['token_csrf'] = bin2hex(random_bytes(32));
}
?>
<form method="post" action="form-berita.php">
<p>
<textarea name="isi" rows="4" cols="40"></textarea>
</p>
<input type="hidden" name="token" value="<?= htmlspecialchars($_SESSION['token_csrf'], ENT_QUOTES, 'UTF-8') ?>">
<p><button type="submit">Kirim</button></p>
</form>
text Keluaran
Berkas ini dirancang untuk berjalan melalui server web.

Pada konteks web, berkas ini menghasilkan form dengan input tersembunyi token yang berisi nilai session. Alur lengkapnya diperiksa dengan server bawaan PHP beserta curl, yaitu mengambil token dari form, mengirim permintaan dengan token yang sah, lalu mengirim permintaan lain dengan token karangan.

bash
# jalankan server bawaan PHP pada port 8100 di latar belakang
php -S localhost:8100 >/dev/null 2>&1 &
sleep 1
# ambil token dari form dan simpan cookie session ke berkas sementara
TOKEN=$(curl -s -c /tmp/jar-csrf.txt http://localhost:8100/form-berita.php | sed -n 's/.*name="token" value="\([0-9a-f]*\)".*/\1/p')
# pastikan form membawa input token tersembunyi
curl -s -b /tmp/jar-csrf.txt http://localhost:8100/form-berita.php | grep -o 'name="token"'
# kirim permintaan dengan token sah beserta cookie session
curl -s -b /tmp/jar-csrf.txt -d "token=$TOKEN&isi=Catatan rapat kelas disimpan" http://localhost:8100/form-berita.php
# kirim permintaan lain dengan token karangan
curl -s -b /tmp/jar-csrf.txt -d "token=0abcdef&isi=Catatan palsu" http://localhost:8100/form-berita.php
# hentikan server
kill %1
text Keluaran
name="token"
Berita tersimpan.
Permintaan ditolak: token CSRF tidak valid.

Permintaan dengan token sah tersimpan, sedangkan permintaan dengan token karangan ditolak dengan kode status 403. Halaman jahat yang tidak mengenal token tidak dapat memalsukan permintaan, sebab cookie yang terkirim otomatis tidak cukup untuk lolos. Token yang sudah dipakai kemudian dihapus dan session ID diputar, sehingga pemakaian ulang token lama pada permintaan berikutnya juga gagal.

Atribut SameSite pada cookie membatasi pengiriman cookie dari konteks lintas situs, sehingga menjadi lapisan pertahanan kedua di samping token. Nilai Lax mengirim cookie hanya untuk navigasi tingkat atas yang aman seperti klik tautan, sedangkan Strict mengirimnya hanya ketika permintaan berasal dari situs yang sama. Nilai None menonaktifkan pembatasan dan menuntut bendera Secure menyertainya.

php
<?php
session_set_cookie_params([
    'lifetime' => 0,
    'path' => '/',
    'httponly' => true,
    'samesite' => 'Lax',
]);
$params = session_get_cookie_params();
echo 'samesite : ' . $params['samesite'] . PHP_EOL;
echo 'httponly : ' . var_export($params['httponly'], true) . PHP_EOL;
text Keluaran
samesite : Lax
httponly : true

Sejak PHP 7.3, parameter session_set_cookie_params menerima array opsi dengan kunci samesite, dan penyetelan dilakukan sebelum session_start agar cookie session yang dikirim browser memuat atribut tersebut. Pengaturan ini mencegah sebagian besar skenario CSRF karena permintaan lintas situs yang dipicu form tersembunyi tidak lagi membawa cookie session.

Kesalahan umum#

Empat kesalahan berikut paling sering muncul saat memasang pertahanan CSRF.

  • Aksi perubahan dijalankan lewat metode GET. Tautan dan gambar lintas situs mudah memicu permintaan GET, sehingga aksi semacam penghapusan dapat berjalan tanpa form. Perbaikannya adalah menempatkan aksi perubahan pada POST dan memverifikasi token sebelum aksi dijalankan.
  • Token dihasilkan dari data yang dapat ditebak, misalnya waktu dan nama pengguna. Token yang dapat ditebak memberi keuntungan yang sama dengan tidak adanya token. Perbaikannya adalah random_bytes(32) sebagai sumber keacakan.
  • Perbandingan token memakai operator ==. Perbandingan biasa berhenti pada byte pertama yang berbeda sehingga rentan terhadap pengukuran waktu. Perbaikannya adalah hash_equals untuk seluruh perbandingan nilai rahasia.
  • Token tidak diputar setelah aksi sah. Token yang sama terus hidup hingga session berakhir, sehingga satu token yang bocor membuka jangka waktu penyalahgunaan yang panjang. Perbaikannya adalah menghapus token setelah dipakai dan memutar session ID pada aksi penting.

Ringkasan#

  • CSRF menyalahgunakan cookie yang terkirim otomatis, sehingga permintaan palsu terlihat sah oleh server.
  • Token CSRF acak disimpan di session, dibawa form sebagai input tersembunyi, dan diverifikasi dengan hash_equals sebelum aksi dijalankan.
  • Permintaan dengan token hilang atau tidak cocok ditolak dengan kode status 403, lalu token diputar agar tidak dipakai ulang.
  • Atribut SameSite pada cookie session membatasi pengiriman lintas situs dan melengkapi, bukan menggantikan, token.

Referensi#