ID session adalah kunci bearer: siapa pun yang membawanya diperlakukan sebagai pemiliknya. Serangan session fixation membalik logika ini — alih-alih mencuri kunci korban, penyerang menanam kunci pilihannya sendiri, lalu menunggu korban login dengan kunci tersebut. Status “sudah login sebagai korban” menempel pada ID yang sudah diketahui penyerang sejak awal, sehingga tidak ada yang perlu dicuri. Chapter ini menyimulasikan serangannya tahap demi tahap dengan ID tetap, lalu membangun pertahanan yang memutus rantainya: regenerasi ID saat login, penutupan pintu URL, dan strict mode yang menolak ID asing. Mekanika session_regenerate_id() secara mendalam sudah dibahas pada chapter Session Regeneration ; fokus kita adalah anatomi serangan dan konfigurasi yang menutupnya.

Anatomi Serangan Session Fixation #

Serangan berjalan dalam empat tahap yang rapi. Pertama, penyerang mendapatkan sebuah ID session yang valid — bisa dengan membuka aplikasi dan mencatat ID miliknya, menyelundupkan cookie dari subdomain yang ia kuasai, atau cukup menebak bila ID aplikasi lemah. Kedua, penyerang memaksa ID itu berpindah ke korban: tautan berparameter seperti app.example/?PHPSESSID=tanaman-123, email berpura-pura promo, atau cookie yang disuntikkan lewat subdomain. Ketiga, korban mengklik tautan dan login — aplikasi menempelkan status login pada ID yang dibawa korban, yaitu ID tanaman penyerang. Keempat, penyerang memakai ID yang sama dan diterima sebagai korban yang sudah login:

Syarat mutlak serangan ini adalah aplikasi bersedia memakai ID yang bukan miliknya: menerima ID dari URL, dari cookie asing, atau dari header mana pun yang bisa dikendalikan pihak luar. Pertahanan mana pun yang memutus salah satu tahap — penanaman, pewarisan, atau penempelan status — sudah meruntuhkan seluruh serangan.

Simulasi: Mewarisi Session Tanaman Penyerang #

Mari kita buktikan anatomi di atas dalam satu skrip. Simulasi memakai penyimpanan session di direktori sementara dan ID tetap agar hasilnya dapat diperiksa; ini_set('session.use_cookies', '0') mematikan cookie di skrip CLI ini agar kita bisa berganti-ganti ID seolah berganti permintaan HTTP:

php
<?php
$dir = sys_get_temp_dir() . '/kursus-php-fixation';
if (!is_dir($dir)) {
    mkdir($dir);
}
session_save_path($dir);
ini_set('session.use_cookies', '0');

$idSerang = 'tanaman-penyerang-123';

// Tahap 1: penyerang "berkunjung", ID yang ia bawa diterima apa adanya
@unlink($dir . '/sess_' . $idSerang);
session_id($idSerang);
session_start();
$_SESSION['keranjang'] = ['Buku PHP'];
session_write_close();

// Tahap 2: korban tiba membawa ID yang sama dari URL buatan penyerang
$_SESSION = [];
session_id($idSerang);
session_start();
$keranjangKorban = $_SESSION['keranjang'] ?? null;

// Tahap 3: korban login - status menempel pada ID tersebut
$_SESSION['user_id'] = 42;
$_SESSION['peran'] = 'admin';
session_write_close();

// Tahap 4: penyerang memakai ID yang sama
$_SESSION = [];
session_id($idSerang);
session_start();
$userIdPenyerang = $_SESSION['user_id'] ?? null;
$peranPenyerang = $_SESSION['peran'] ?? null;
session_destroy();
rmdir($dir);

echo 'Tahap 2 - keranjang korban : ' . json_encode($keranjangKorban) . "\n";
echo 'Tahap 4 - user_id penyerang : ' . var_export($userIdPenyerang, true) . "\n";
echo 'Tahap 4 - peran penyerang   : ' . var_export($peranPenyerang, true) . "\n";

Output yang Dihasilkan:

text
Tahap 2 - keranjang korban : ["Buku PHP"]
Tahap 4 - user_id penyerang : 42
Tahap 4 - peran penyerang   : 'admin'

Hasil eksekusinya menceritakan seluruh serangan: korban mewarisi keranjang yang ditanam penyerang pada Tahap 2, lalu pada Tahap 4 penyerang membuka ID yang sama dan mendapati dirinya sudah login sebagai user_id 42 dengan peran admin — tanpa pernah tahu kata sandi korban. Baris $_SESSION = []; sebelum tiap session_start() hanya kebersihan simulasi (mensimulasikan request baru yang memori $_SESSION-nya masih kosong); bintang masalahnya adalah session_id($idSerang) yang menerima ID dari luar tanpa bertanya. Perhatikan pula bahwa serangan tidak butuh cookie dicuri — ID-nya sejak awal milik penyerang.

Pertahanan Utama: Regenerasi ID Saat Login #

Pertahanan yang menuntaskan Tahap 3 adalah aturan besi session: setiap kenaikan hak (login, eskalasi peran) wajib mengganti ID. session_regenerate_id(true) menerbitkan ID acak baru, memindahkan seluruh data ke ID baru, dan menghapus ID lama — kunci tanaman kehilangan nilai seketika:

php
<?php
$dir = sys_get_temp_dir() . '/kursus-php-fixation-regen';
if (!is_dir($dir)) {
    mkdir($dir);
}
session_save_path($dir);

$idTanam = 'tanaman-penyerang-456';
@unlink($dir . '/sess_' . $idTanam);

// Korban tiba dengan ID tanaman penyerang
session_id($idTanam);
session_start();
$_SESSION['keranjang'] = ['Buku PHP'];
$idSebelum = session_id();

// Login sukses: ID langsung diganti
session_regenerate_id(true);
$idSesudah = session_id();
$_SESSION['user_id'] = 42;

echo 'ID sama setelah login  : ' . var_export($idSesudah === $idSebelum, true) . "\n";
echo 'Keranjang bertahan     : ' . var_export($_SESSION['keranjang'] === ['Buku PHP'], true) . "\n";
echo 'Status login menempel  : ' . var_export(($_SESSION['user_id'] ?? null) === 42, true) . "\n";
echo 'File ID lama terhapus  : ' . var_export(!file_exists($dir . '/sess_' . $idSebelum), true) . "\n";

session_destroy();
rmdir($dir);

Output yang Dihasilkan:

text
ID sama setelah login  : false
Keranjang bertahan     : true
Status login menempel  : true
File ID lama terhapus  : true

Empat baris hasilnya adalah bukti bahwa pertahanan ini bekerja tanpa merusak pengalaman korban: ID berubah, keranjang belanja tetap ada, status login menempel pada ID baru, dan file penyimpanan ID lama sudah dihapus. Penyerang yang kembali memakai tanaman-penyerang-456 menemukan pintu terkunci — ID itu tidak lagi membuka apa pun. Nilai true pada session_regenerate_id(true) penting: tanpa argumen tersebut, file ID lama dibiarkan hidup dan penyerang tetap memegang session valid (kini tanpa status login, tetap jendela yang tidak perlu).

Matikan Pintu URL: use_only_cookies #

Serangan pada simulasi kita masuk lewat pintu yang di zaman modern seharusnya sudah digerbangi. Tiga direktif php.ini mengatur dari mana PHP bersedia membaca ID session:

php
<?php
echo 'session.use_only_cookies : ' . ini_get('session.use_only_cookies') . "\n";
echo 'session.use_strict_mode  : ' . ini_get('session.use_strict_mode') . "\n";
echo 'session.use_trans_sid    : ' . ini_get('session.use_trans_sid') . "\n";

Output yang Dihasilkan:

text
session.use_only_cookies : 1
session.use_strict_mode  : 0
session.use_trans_sid    : 0

session.use_only_cookies = 1 (bawaan PHP modern) menyatakan ID hanya dibaca dari cookie — parameter ?PHPSESSID= di URL diabaikan sepenuhnya, dan session.use_trans_sid = 0 memastikan PHP tidak pernah menulis ID ke URL. Dua direktif ini menutup jalur penanaman paling umum; ID di URL juga bocor ke riwayat browser, log server, dan header Referer, jadi menutupnya sekaligus mengurangi kebocoran. Namun pintu cookie sendiri belum sepenuhnya aman: penyerang masih bisa menanam cookie dari subdomain yang dikendalikannya (atribut Domain yang terlalu longgar) atau lewat jaringan yang disadap bila cookie tidak berflag Secure dan HttpOnly — pembahasan flag-flag ini ada di chapter Cookies .

Strict Mode: Tolak ID yang Tak Dikenal #

Nilai bawaan pada keluaran di atas menyisakan satu celah: session.use_strict_mode = 0. Artinya, PHP menerima ID apa pun yang dibawa browser — bila file penyimpanannya belum ada, PHP membuatkan file baru untuk ID itu. Persis perilaku yang dieksploitasi simulasi kita. Strict mode membaliknya: ID yang tidak dikenal ditolak dan diganti dengan ID baru yang diterbitkan server:

php
<?php
$dir = sys_get_temp_dir() . '/kursus-php-fixation-strict';
if (!is_dir($dir)) {
    mkdir($dir);
}
session_save_path($dir);
ini_set('session.use_strict_mode', '1');

$idPalsu = 'penyerang-pilih-id-789';
@unlink($dir . '/sess_' . $idPalsu);

session_id($idPalsu);
session_start();
$idNyata = session_id();

echo 'ID penyerang diterima?  : ' . var_export($idNyata === $idPalsu, true) . "\n";
echo 'File ID penyerang dibuat? : ' . var_export(file_exists($dir . '/sess_' . $idPalsu), true) . "\n";
echo 'ID pengganti panjang 32 : ' . var_export(strlen($idNyata) === 32, true) . "\n";

session_destroy();
rmdir($dir);

Output yang Dihasilkan:

text
ID penyerang diterima?  : false
File ID penyerang dibuat? : false
ID pengganti panjang 32 : true

Hasilnya tegas: ID buatan penyerang tidak pernah diterima, file untuk ID itu tidak pernah dibuat, dan yang dipakai adalah ID acak panjang 32 karakter hasil terbitan server. Dengan strict mode, satu-satunya cara memperoleh ID valid adalah diberi langsung oleh server — penanaman ID pilihan sendiri menjadi mustahil. Direktif ini sebaiknya di-set di php.ini atau session_set_cookie_params()-level sebelum session_start() pertama, karena perilaku PHP ditentukan saat session dimulai; detail seluruh direktif keamanan session terdokumentasi di halaman resmi php.net berikut.

Kombinasi Pertahanan #

Empat pertahanan di atas menutup tahap yang berbeda dari serangan yang sama. Tabel berikut merangkum siapa menutup apa:

PertahananTahap serangan yang ditutup
session_regenerate_id(true) saat loginstatus login tak pernah menempel pada ID tanaman
use_only_cookies = 1ID dari URL dan POST diabaikan
use_strict_mode = 1ID asing ditolak, selalu diterbitkan server
Secure + HttpOnly pada cookieID tidak terbaca skrip dan tidak tersadap di jaringan

Tidak ada satu baris pun yang menggantikan baris lain — regenerasi tanpa strict mode masih membiarkan penanaman, strict mode tanpa cookie flag masih membuka penyadapan. Prinsip yang sama berlaku pada level framework: Laravel dan Symfony menjalankan kombinasi ini secara bawaan, tetapi memahami apa yang mereka lakukan tetap tugas kita, karena satu ini_set yang salah di server bisa membuka kembali celah yang sudah tertutup.

Ringkasan #

Session fixation mengajarkan bahwa keamanan ID session bukan soal kerahasiaan semata, melainkan soal asal-usul: ID yang valid harus lahir dari server, bukan dibawa dari luar. Penyerang menanam ID pilihannya, korban memberinya status login, dan penyerang tinggal memanennya — seluruh rantai itu patah saat aplikasi menolak ID asing dan mengganti ID pada setiap perubahan hak. Dengan session kini aman, section ini bergerak ke pertahanan lapisan HTTP: bagaimana header seperti X-Frame-Options dan Content-Security-Policy melindungi pengguna di level browser, mulai dari chapter Clickjacking.

Referensi #