Session PHP yang Aman untuk Aplikasi Kecil — Lindungi ID dari Login sampai Logout
Form login bisa memverifikasi password yang benar, tetapi akun tetap bertumpu pada session yang lemah. Bagian sulitnya justru dimulai setelah autentikasi: bagaimana aplikasi mengenali request berikutnya, berapa lama pengenalan itu boleh berlaku, dan apa yang terjadi saat pengguna logout?
Untuk aplikasi native PHP kecil, jawabannya sering berupa session server-side yang identifier-nya dikirim melalui cookie. Ini desain yang masuk akal, tetapi identifier tersebut untuk sementara hampir sama bergunanya dengan credential yang membuatnya. Karena itu, OWASP menggambarkan session ID terautentikasi sebagai sesuatu yang sementara waktu setara dengan metode autentikasi di belakangnya. Jika seseorang dapat mencuri, memprediksi, atau menetapkan identifier itu melalui session fixation, pemeriksaan password mungkin tidak lagi melindungi request setelahnya.
Ini bukan resep untuk “session yang aman secara sempurna.” Tidak ada satu konfigurasi yang bisa menjanjikan hal itu. Artikel ini adalah cara praktis untuk meninjau seluruh lifecycle: membuat ID, membawanya dalam cookie, merotasinya ketika privilege berubah, mengakhirinya di server, dan menghapusnya saat logout.
Mulai dari lifecycle session, bukan daftar flag
Model mental yang berguna adalah tiket penitipan barang. Browser memegang tiket; aplikasi menyimpan barang dan informasi terkait. Tiket itu semestinya tidak mengungkapkan informasi berharga, tetapi siapa pun yang memegang tiket valid dapat dianggap sebagai pemiliknya. Inilah alasan opaque session ID tetap harus dilindungi dengan cermat.
Lifecycle ini setidaknya memiliki lima transisi:
- Request anonim menerima atau melanjutkan sebuah session.
- Login yang berhasil mengubah session dari anonim menjadi terautentikasi.
- Request biasa terus memakai session terautentikasi tersebut.
- Periode tidak aktif atau batas waktu absolut membuat session tidak valid.
- Logout menghapus state aplikasi, cookie browser, dan record server-side.
Cookie yang aman pada langkah pertama tidak memperbaiki rotasi ID yang hilang pada langkah kedua. Idle timeout yang baik juga tidak memperbaiki handler logout yang meninggalkan cookie di browser. Meninjau setiap transisi membantu menemukan celah yang dapat tersembunyi di balik checklist pengaturan terpisah.
Perkuat session sebelum memulainya
PHP memungkinkan konfigurasi session melalui php.ini, konfigurasi pool, atau code aplikasi. Ketika aplikasi memakai session_set_cookie_params(), manual PHP mewajibkan fungsi itu dijalankan sebelum session_start() dan menjelaskan bahwa efeknya hanya berlaku selama script saat ini. Bootstrap terpusat yang disertakan oleh setiap route terautentikasi lebih aman daripada mengulang pengaturan yang sedikit berbeda di berbagai controller.
<?php
declare(strict_types=1);
ini_set('session.use_strict_mode', '1');
ini_set('session.use_only_cookies', '1');
ini_set('session.use_trans_sid', '0');
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
]);
session_start();
Contoh ini mengasumsikan situs hanya disajikan melalui HTTPS. Pada origin development dengan HTTP biasa, cookie Secure tidak akan berperilaku seperti cookie production. Karena itu, pengujian sebaiknya memakai HTTPS lokal, bukan melemahkan pengaturan production.
Apa yang sebenarnya dilakukan setiap pilihan
session.use_strict_mode memberi tahu PHP agar tidak mengadopsi session ID yang belum dibuat oleh session handler. PHP menyebut strict mode sebagai pengaturan penting untuk memperkuat session, sekaligus menjelaskannya sebagai mitigasi, bukan jawaban lengkap untuk session fixation. session.use_only_cookies mencegah PHP menerima identifier melalui mekanisme alternatif seperti query atau parameter form. Ini juga menjauhkan ID dari URL, riwayat browser, link yang disalin, dan access log umum.
Atribut cookie mengurangi risiko yang berbeda:
Securemembatasi pengiriman ke koneksi aman.HttpOnlymencegah script browser membaca cookie melalui API sepertidocument.cookie.SameSite=Laxmembatasi sebagian pengiriman cookie lintas situs sambil mempertahankan alur navigasi tingkat atas yang umum.
Kontrol ini lebih terbatas daripada yang mungkin tersirat dari namanya. HttpOnly tidak menyembuhkan cross-site scripting: script yang disisipkan masih dapat melakukan tindakan dari halaman korban meski tidak dapat membaca ID. SameSite adalah defense in depth, bukan pengganti CSRF token. Secure memerlukan HTTPS untuk seluruh session, bukan hanya response login.
Contoh ini sengaja tidak menyertakan Domain. Berdasarkan aturan cookie dalam RFC 6265, menghilangkannya membuat cookie terbatas pada host yang menetapkannya. Menambahkan parent domain membuat cookie tersedia bagi subdomain dan memperluas jumlah aplikasi yang dapat memengaruhinya. Scope yang lebih luas seharusnya menjadi kebutuhan eksplisit, bukan pilihan default demi kemudahan.
Lifetime nol membuat cookie non-persistent, tetapi jangan mengartikannya sebagai “pengguna pasti logout ketika tab terakhir ditutup.” MDN menjelaskan bahwa browser menentukan kapan sebuah session berakhir, dan session restoration dapat mempertahankan cookie seperti ini. Expiry server-side tetap diperlukan.
Rotasi ID ketika tingkat kepercayaan berubah
Session ID yang dipakai pengunjung anonim seharusnya tidak langsung menjadi ID terautentikasi. Setelah credential diverifikasi, rotasi ID sebelum menganggap request berikutnya sudah terautentikasi:
if ($credentialsAreValid) {
if (!session_regenerate_id(false)) {
throw new RuntimeException('Could not rotate the session ID');
}
$_SESSION['user_id'] = $userId;
$_SESSION['authenticated_at'] = time();
$_SESSION['last_activity_at'] = time();
}
Rotasi penting setelah login dan perubahan privilege lain, misalnya saat masuk ke mode administrator. Langkah ini memutus urutan session fixation sederhana: penyerang memberikan ID anonim yang sudah diketahui, menunggu korban mengautentikasinya, lalu menggunakan ulang ID tersebut.
Argumen boolean pada fungsi itu perlu perhatian lebih daripada yang diberikan banyak contoh singkat. Dokumentasi PHP memperingatkan bahwa langsung menghapus data session lama dapat menyebabkan session hilang atau state tidak konsisten ketika ada request bersamaan atau jaringan tidak stabil. Membiarkan record lama tanpa batas juga tidak diinginkan. Aplikasi berisiko tinggi atau sangat concurrent memerlukan desain transisi eksplisit: tandai ID lama sebagai obsolete, izinkan overlap singkat hanya jika dibutuhkan, tolak setelah jendela itu, dan catat penggunaan ulang yang mencurigakan tanpa mencatat ID mentah.
Contoh kecil ini memakai false agar tidak mengklaim bahwa penghapusan seketika selalu aman. Ini bukan implementasi revocation yang lengkap. Aplikasi dengan request AJAX paralel, banyak worker, atau handler Redis/database kustom harus merancang dan menguji transisi ini terhadap model storage dan concurrency yang benar-benar digunakan.
Terapkan expiry dalam logic aplikasi
Expiry cookie mengatur berapa lama browser boleh mempertahankan cookie. Garbage collection PHP mengatur kapan record server-side lama dapat dihapus. Keduanya tidak secara mandiri menyatakan kebijakan authorization aplikasi. Manual PHP secara khusus mengingatkan agar session.gc_maxlifetime tidak dijadikan mekanisme expiry yang terjamin dan menyarankan pengelolaan lifetime berbasis timestamp.
Dua deadline menjawab pertanyaan yang berbeda:
- Idle timeout menentukan berapa lama session terautentikasi boleh tidak digunakan.
- Absolute timeout menentukan usia maksimal session meskipun terus aktif.
$now = time();
$idleLimit = 30 * 60;
$absoluteLimit = 8 * 60 * 60;
$idleExpired = isset($_SESSION['last_activity_at'])
&& $now - $_SESSION['last_activity_at'] > $idleLimit;
$absoluteExpired = isset($_SESSION['authenticated_at'])
&& $now - $_SESSION['authenticated_at'] > $absoluteLimit;
if ($idleExpired || $absoluteExpired) {
// Run the same complete invalidation used by logout, then require login.
} else {
$_SESSION['last_activity_at'] = $now;
}
Angka di sini menggambarkan mekanisme, bukan kebijakan universal. Dashboard editorial, session preferensi publik, dan layar persetujuan pembayaran memiliki konsekuensi serta biaya usability yang berbeda. Pilih batas berdasarkan sensitivitas tindakan, perilaku pengguna yang diperkirakan, dan ketersediaan reauthentication. Simpan timestamp di server, bandingkan pada setiap protected request, dan gunakan waktu server yang konsisten.
Logout memiliki tiga lapisan
session_destroy() terdengar lengkap, tetapi perilaku yang didokumentasikan lebih sempit: fungsi ini menghancurkan data yang terkait dengan session saat ini dan tidak mengosongkan $_SESSION maupun menghapus cookie session. Logout lengkap biasanya perlu membersihkan ketiga lapisan.
$_SESSION = [];
if (ini_get('session.use_cookies')) {
$params = session_get_cookie_params();
setcookie(session_name(), '', [
'expires' => time() - 42000,
'path' => $params['path'],
'domain' => $params['domain'],
'secure' => $params['secure'],
'httponly' => $params['httponly'],
'samesite' => $params['samesite'] ?: 'Lax',
]);
}
session_destroy();
Cookie penghapusan harus memakai nama yang sama serta scope path/domain yang kompatibel dengan cookie awal. Jika tidak, browser dapat mempertahankan cookie aslinya. Request bersamaan juga tetap menjadi caveat: satu request yang masih berjalan dapat berinteraksi dengan state ketika request lain sedang logout. Sistem sensitif mungkin membutuhkan record revocation server-side atau pengelolaan session seluruh akun, bukan hanya mengandalkan penghapusan satu record.
Verifikasi perilaku, jangan hanya mempercayai konfigurasi
Review dapat dilakukan tanpa menyerang aplikasi:
- Periksa response login di developer tools browser. Pastikan cookie session memiliki
Secure,HttpOnly, nilaiSameSiteyang dimaksud, dan tidak memilikiDomainyang tidak diperlukan. - Catat hanya apakah ID berubah, bukan nilai ID itu sendiri. ID semestinya berotasi setelah login berhasil dan perubahan privilege lainnya.
- Kirim session ID buatan yang tidak dikenal ke test environment. Dengan strict mode dan handler yang dimaksud, PHP seharusnya menerbitkan atau memakai ID valid buatan server, bukan mengadopsi ID yang diberikan.
- Majukan timestamp pengujian yang dikendalikan untuk memeriksa idle expiry dan absolute expiry secara terpisah.
- Lakukan logout, lalu pastikan cookie browser terhapus dan replay cookie pengujian lama tidak dapat mengakses protected route.
- Jalankan pengujian concurrent request di sekitar rotasi dan logout jika interface membuat request paralel.
Log sebaiknya mencatat event seperti creation, rotation, alasan expiry, dan logout, tetapi bukan session ID mentah. Hashing ID untuk correlation pun masih dapat menciptakan data sensitif yang bisa ditautkan, sehingga retention dan aksesnya perlu dibatasi.
Hal yang tidak diselesaikan oleh session hardening
Session yang dikelola dengan baik tetap dapat mengizinkan tindakan yang salah jika aplikasi memiliki broken access control. Session juga masih bisa digunakan oleh script berbahaya yang berjalan di halaman. Pengaturan ini tidak menggantikan output encoding, perlindungan CSRF, TLS, keamanan password, rate limiting, atau reauthentication sebelum tindakan yang sangat sensitif.
Mengikat session secara kaku ke IP address juga bukan solusi sederhana. Jaringan seluler, proxy, dan alamat yang berubah dapat menolak pengguna sah, sementara penyerang mungkin terlihat memakai jalur jaringan yang sama. Sinyal seperti perubahan IP dapat membantu risk detection, tetapi memperlakukannya sebagai bukti identitas membutuhkan pengujian yang hati-hati.
Session adalah proses, bukan sekadar flag cookie
Perbaikan terkuat bukan satu opsi ajaib, melainkan konsistensi sepanjang lifecycle: terima hanya ID buatan server, bawa ID hanya dalam secure cookie dengan scope sempit, rotasi ketika tingkat kepercayaan berubah, terapkan timeout di server, dan hapus setiap lapisan saat logout.
Tetap ada keputusan desain seputar concurrency dan invalidation ID lama, sedangkan timeout yang tepat bergantung pada aplikasinya. Membuat keputusan itu eksplisit lebih jujur dan lebih berguna daripada menyalin snippet “secure session” lalu menganggap pekerjaannya selesai.
