Session adalah kunci kerajaan: siapa pun yang memegang ID session yang sah akan diperlakukan sebagai pemiliknya. Konsekuensinya, keamanan aplikasi bertumpu pada seberapa rapat kita menjaga ID itu — dari cara session dibuat, dikirim, diumurkan, sampai diakhiri. Chapter ini merangkum praktik pengamanan session dalam satu panduan utuh: regenerasi ID setelah perubahan hak akses, flag cookie httponly/secure/samesite, timeout idle dan absolut, hingga kebiasaan menimbang apa yang boleh disimpan di $_SESSION.
Peta Ancaman terhadap Session #
Sebelum bertahan, kita perlu mengenali arah serangan. Ada tiga vektor utama yang menyasar session PHP. Session fixation: penyerang menanamkan ID session yang sudah diketahui ke korban (misalnya lewat URL), lalu menunggu korban login dengan ID tersebut sehingga penyerang tinggal memakai ID yang sama. Session theft/hijacking: ID session bocor — melalui jaringan yang tidak terenkripsi, skrip jahat di browser, atau header Referer — dan dipakai pihak lain. Kebocoran data: isi $_SESSION yang sensitif ikut tumpah ketika penyimpanan server atau file session terbaca orang yang tidak berhak.
Ketiganya ditangkal oleh kombinasi pertahanan berlapis yang akan kita bangun satu per satu. Tidak ada satu pengaturan ajaib; yang ada adalah kebiasaan kecil yang saling menguatkan.
Regenerasi ID dan Penolakan ID dari URL #
Pertahanan pertama sudah kita miliki sejak chapter Session Regeneration
: panggil session_regenerate_id(true) setiap kali hak akses berubah — terutama tepat setelah login. Dengan ID baru yang diterbitkan saat login, ID lama yang mungkin sudah tertanam di browser korban menjadi tidak berharga. Aturan pendampingnya: jangan pernah menerima ID session dari URL atau input pengguna, karena di situlah fixation biasanya ditanamkan:
<?php
// Tolak keras ID session yang coba masuk lewat URL/?GET
if (isset($_GET['PHPSESSID'])) {
header('Location: /login');
exit;
}
session_start();
if ($loginBerhasil) {
session_regenerate_id(true); // ID baru, ID lama dibuang
$_SESSION['user_id'] = $userId;
}Blok di atas berjalan di lingkungan web sungguhan (butuh request HTTP nyata), jadi kita pahami lewat alurnya: penolakan $_GET['PHPSESSID'] memutus jalur penanaman ID, dan session_regenerate_id(true) memastikan session pasca-login memakai identitas baru. PHP juga menyediakan direktif session.use_only_cookies = 1 (bawaan sejak PHP 5.3 ke atas) yang membuat PHP sama sekali mengabaikan ID dari URL.
Flag Cookie httponly, secure, dan samesite #
ID session dikirim bolak-balik lewat cookie, sehingga pengaturan cookie adalah pertahanan kedua. Tiga flag wajib dipasang sebelum session_start() — penjelasan lengkap masing-masing ada di chapter Cookie Options
. httponly melarang JavaScript membaca cookie (meredam pencurian lewat XSS), secure membatasi pengiriman hanya lewat HTTPS (meredam pencurian di jaringan), dan samesite menahan cookie agar tidak ikut terkirim dari situs lain (meredam CSRF):
<?php
ini_set('session.cookie_httponly', '1');
ini_set('session.cookie_secure', '1');
ini_set('session.cookie_samesite', 'Lax');
echo 'session.cookie_httponly: ' . ini_get('session.cookie_httponly') . "\n";
echo 'session.cookie_secure: ' . ini_get('session.cookie_secure') . "\n";
echo 'session.cookie_samesite: ' . ini_get('session.cookie_samesite') . "\n";Output yang Dihasilkan:
session.cookie_httponly: 1
session.cookie_secure: 1
session.cookie_samesite: LaxPada aplikasi nyata, ketiganya lebih tepat dipasang permanen di php.ini atau session_set_cookie_params() daripada ini_set() per skrip, supaya tidak ada satu halaman pun yang lupa. Nilai Lax adalah pilihan seimbang untuk aplikasi web umum; API murni yang tidak memakai cookie bahkan tidak terpengaruh flag ini.
Idle Timeout dengan last_activity #
Pertahanan ketiga membatasi kerusakan: session yang dicuri seharusnya tidak bisa dipakai selamanya. Idle timeout memutus session yang tidak menunjukkan aktivitas dalam jangka waktu tertentu. Polanya: setiap request mencatat waktu aktivitas terakhir ke $_SESSION['last_activity'], dan setiap request berikutnya membandingkan selisihnya terhadap batas:
<?php
$dir = sys_get_temp_dir() . '/php-kursus-sesi';
if (!is_dir($dir)) {
mkdir($dir);
}
session_save_path($dir);
session_id('demo-idle-timeout');
@unlink($dir . '/sess_demo-idle-timeout');
session_start();
// Waktu dibekukan agar contoh deterministik
$aktivitasTerakhir = new DateTimeImmutable('2026-01-15 08:00:00', new DateTimeZone('Asia/Jakarta'));
$requestMasuk = new DateTimeImmutable('2026-01-15 08:40:00', new DateTimeZone('Asia/Jakarta'));
$batasIdle = 1800; // 30 menit
$selisih = $requestMasuk->getTimestamp() - $aktivitasTerakhir->getTimestamp();
if ($selisih > $batasIdle) {
session_unset();
session_destroy();
echo "Idle $selisih detik melebihi batas $batasIdle detik: session dihancurkan, wajib login ulang\n";
} else {
$_SESSION['last_activity'] = $requestMasuk->getTimestamp();
echo "Idle $selisih detik masih di bawah batas: session diterima, last_activity diperbarui\n";
}Output yang Dihasilkan:
Idle 2400 detik melebihi batas 1800 detik: session dihancurkan, wajib login ulangPada aplikasi nyata, time() menggantikan kedua DateTimeImmutable beku tersebut — pola lain tetap sama. Cabang if membersihkan session dengan session_unset() dan session_destroy() sebagaimana pola logout di chapter Sessions
, lalu mengarahkan pengguna ke halaman login; cabang else cukup menimpa last_activity dengan waktu request ini. Batas 30 menit adalah titik awal yang lazim untuk aplikasi umum; aplikasi perbankan memilih jauh lebih pendek.
Absolute Timeout Membatasi Umur Total #
Idle timeout punya celah: penyerang yang mencuri cookie dapat tetap “merawat” session dengan mengirim request berkala sehingga last_activity selalu segar. Absolute timeout menutup celah ini dengan membatasi umur total sejak session dibuat, tanpa peduli seberapa aktif pemakainya. Praktiknya, catat juga created_at di session dan tolak session yang umurnya melebihi batas:
<?php
$sessionDibuat = new DateTimeImmutable('2026-01-15 08:00:00', new DateTimeZone('Asia/Jakarta'));
$terakhirAktif = new DateTimeImmutable('2026-01-15 11:55:00', new DateTimeZone('Asia/Jakarta'));
$requestMasuk = new DateTimeImmutable('2026-01-15 12:01:00', new DateTimeZone('Asia/Jakarta'));
$batasIdle = 1800; // 30 menit
$batasAbsolute = 14400; // 4 jam
$idle = $requestMasuk->getTimestamp() - $terakhirAktif->getTimestamp();
$umur = $requestMasuk->getTimestamp() - $sessionDibuat->getTimestamp();
echo "Idle: $idle detik (batas $batasIdle)\n";
echo "Umur session: $umur detik (batas $batasAbsolute)\n";
echo 'Keputusan: ' . match (true) {
$idle > $batasIdle => 'tolak karena idle timeout',
$umur > $batasAbsolute => 'tolak karena absolute timeout, wajib login ulang',
default => 'terima dan perbarui last_activity',
} . "\n";Output yang Dihasilkan:
Idle: 360 detik (batas 1800)
Umur session: 14460 detik (batas 14400)
Keputusan: tolak karena absolute timeout, wajib login ulangSkenario di atas sengaja memilih angka yang menggugah: idle hanya 360 detik — jauh di bawah batas 30 menit — tetapi umur session sudah 14460 detik, melewati batas 4 jam. Ekspresi match (true) mengevaluasi idle timeout lebih dulu; hanya bila lolos, absolute timeout diperiksa. Kombinasi keduanya berarti session yang sehat paling lama berumur 4 jam dan paling malas aktif 30 menit.
Binding ke User Agent dan Batasnya #
Pertahanan tambahan yang sering dibahas adalah mengikat session ke fingerprint perangkat — biasanya hash dari header User-Agent. Saat login, fingerprint disimpan di session; setiap request berikutnya membandingkannya, dan ketidakcocokan dianggap tanda session dipakai di perangkat lain:
<?php
$uaSaatLogin = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/131.0 Safari/537.36';
$uaRequestIni = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/131.0 Safari/537.36';
$uaPerangkatLain = 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_1 like Mac OS X) Safari/605.1.15';
$fingerprintLogin = hash('sha256', $uaSaatLogin);
echo 'User agent sama: ' . var_export(hash_equals($fingerprintLogin, hash('sha256', $uaRequestIni)), true) . "\n";
echo 'User agent perangkat lain: ' . var_export(hash_equals($fingerprintLogin, hash('sha256', $uaPerangkatLain)), true) . "\n";Output yang Dihasilkan:
User agent sama: true
User agent perangkat lain: falsePenting dicatat bahwa pertahanan ini lemah dan hanya lapisan tambahan. Header User-Agent dikirim oleh klien sehingga penyerang yang mencuri cookie tinggal menyalin header korban juga — binding tidak bisa membedakan mereka. Di sisi lain, binding menghasilkan false positive: pembaruan browser kecil bisa mengubah string user agent dan menggugurkan session pengguna yang jujur. Perlakukan binding sebagai sinyal untuk mencurigai (misalnya memaksa re-autentikasi), bukan voni.
Minimalisasi Data Session #
Pertahanan terakhir ada pada isi session itu sendiri: simpan sesedikit mungkin, dan jangan pernah data yang sensitif dalam bentuk terbaca. File session di server adalah berkas biasa — satu salah konfigurasi direktori atau satu celah arbitrary file read, seluruh isinya terbaca. Prinsipnya sederhana:
| Simpan | Jangan simpan | Alasan |
|---|---|---|
user_id (integer) | username dan password (bahkan hash) | user_id cukup untuk memuat data lain dari database |
| peran/ID izin seperlunya | data kartu kredit lengkap | data pembayaran milik payment processor |
| preferensi ringan | isi keranjang raksasa | session bukan gudang data |
flag status (is_verified) | token API pihak ketiga | token rahasia milik tempat lain |
Data yang dibutuhkan selalu bisa diambil ulang dari database berdasarkan user_id di session. Mengosongkan session segera setelah tidak diperlukan — session_unset() saat logout — juga memperkecil jendela paparan.
Garbage Collection dan Transport yang Aman #
Dua pengaman penutup bekerja di luar kode aplikasi. Pertama, session.gc_maxlifetime menentukan seberapa lama PHP menyimpan session yang ditinggalkan — bawaannya 1440 detik — dan sebaiknya diset sedikit lebih longgar daripada idle timeout aplikasi agar tidak saling bertabrakan; detail direktifnya dibahas di chapter Session Configuration
. Ingat bahwa GC berjalan probabilistik, jadi penghapusan tidak terjadi tepat waktu — timeout aplikasi seperti di atas tetap menjadi penjaga yang sesungguhnya. Kedua, seluruh lalu lintas harus lewat HTTPS; tanpa enkripsi, flag secure saja tidak berarti apa-apa karena ID session dapat disadap di jaringan sebelum sampai ke server. Sertifikat TLS kini murah (bahkan gratis via Let’s Encrypt), dan HSTS dapat dipakai agar browser menolak versi HTTP sama sekali.
Ringkasan #
Session yang aman adalah hasil pertahanan berlapis: session_regenerate_id(true) setelah login memutus fixation, flag httponly/secure/samesite melindungi cookie di perjalanan, idle dan absolute timeout membatasi umur session yang dicuri, dan minimalisasi isi $_SESSION memperkecil kerusakan saat kebocoran terjadi. Binding ke user agent boleh dipakai sebagai sinyal tambahan selama kelemahannya dipahami. Dengan seluruh perlengkapan section ini, kita siap melihat gambar besar terakhir: bagaimana session membuat aplikasi bersifat stateful, dan kapan justru desain stateless yang lebih tepat — bahasan chapter penutup.