Open Redirect pada Aplikasi PHP Kecil - Biarkan Server Memilih Tujuan
Form login sering perlu mengembalikan seseorang ke halaman yang semula ingin dibuka. Checkout mungkin berlanjut ke tanda terima, sedangkan tindakan admin mungkin kembali ke sebuah daftar. Parameter seperti next atau return_to tampak sebagai cara praktis untuk mempertahankan alur tersebut. Pertanyaan keamanannya bukan apakah aplikasi boleh melakukan redirect. Pertanyaannya: siapa yang diizinkan memilih tujuan?
Jika data request dapat memasok tujuan apa pun, fitur navigasi yang berguna dapat berubah menjadi open redirect. URL pertama tetap milik aplikasi yang sah, tetapi responsnya dapat mengirim browser ke tempat yang tidak pernah dimaksudkan oleh aplikasi. Desain yang lebih aman biasanya bukan upaya yang lebih pintar untuk mengenali setiap URL buruk. Caranya adalah memberi input tidak tepercaya wewenang yang lebih kecil.
Apa yang membuat sebuah redirect menjadi terbuka?
CWE-601 menjelaskan kelemahan intinya sebagai aplikasi yang menerima input yang dikendalikan pengguna untuk menentukan tautan eksternal, lalu memakai tautan itu dalam redirect. Cheat sheet OWASP membingkainya dengan cara serupa: input tidak tepercaya mencapai tujuan tanpa keputusan yang memadai tentang apakah tujuan itu valid dan sesuai.
Di PHP, bentuk rentan yang paling singkat mudah dikenali:
<?php
$next = $_GET['next'] ?? '/';
header('Location: ' . $next);
exit;
Masalahnya bukan header() itu sendiri. Masalahnya adalah jalur tanpa penghalang dari parameter request menuju field Location. Query parameter adalah sumber yang paling jelas, tetapi pertanyaan kepercayaan yang sama berlaku untuk form field, cookie, request header, nilai database yang semula dipasok pengguna, atau respons dari layanan lain. Memindahkan nilai ke penyimpanan tidak membuatnya tepercaya.
Konteks tetap penting. Layanan yang sengaja dirancang untuk mengarahkan pengguna ke tautan mana pun memiliki kebutuhan berbeda dari alur login CMS. CWE secara eksplisit mencatat bahwa perilaku yang dimaksud memengaruhi apakah hal ini merupakan kerentanan. Namun, untuk alur kembali biasa, navigasi lintas origin secara sewenang-wenang umumnya memberi wewenang lebih besar daripada yang dibutuhkan fitur.
Dampaknya sering berkaitan dengan kepercayaan yang dipinjam
Penyerang dapat menempatkan domain yang sah di awal tautan sambil mengatur agar tujuan akhirnya berupa halaman yang tidak tepercaya. OWASP dan CWE menyebut phishing sebagai salah satu kemungkinan akibat: domain pertama yang familier dapat membuat tautan tampak lebih meyakinkan, meskipun browser meninggalkannya setelah menerima respons. Ini tidak berarti setiap open redirect memiliki tingkat keparahan yang sama atau pasti mencuri kredensial. Dampaknya bergantung pada lokasi redirect, harapan pengguna, dan apakah workflow lain bergantung pada tujuan tersebut.
Redirect juga tidak boleh menggantikan authorization. Mengirim pengguna yang tidak berwenang menjauh dari route admin tidak cukup jika route itu menjalankan tindakan sebelum melakukan redirect, atau jika request lain dapat mencapai handler yang sama secara langsung. Endpoint tujuan harus menerapkan aturan aksesnya sendiri. Navigasi menentukan ke mana browser pergi; authorization menentukan apa yang diizinkan server.
Utamakan nama tujuan daripada URL tujuan
Jika sebuah alur memiliki tiga hasil yang valid, terima tiga identifier alih-alih seluruh ruang URL. OWASP dan CWE sama-sama menyarankan pemetaan nama pendek, ID, atau token ke tujuan yang dikendalikan server. Request boleh memilih di antara opsi yang disetujui, tetapi tidak dapat menciptakan opsi keempat.
Sebagai contoh, anggap sebuah POST yang berhasil boleh berlanjut ke dashboard atau halaman keamanan akun:
<?php
$destinations = [
'dashboard' => '/dashboard',
'security' => '/account/security',
];
$key = $_POST['next'] ?? 'dashboard';
$target = $destinations[$key] ?? $destinations['dashboard'];
header('Location: ' . $target, true, 303);
exit;
Contoh ini sengaja dibuat sempit. Authentication, perlindungan CSRF, dan tindakan yang dikirim tetap membutuhkan kontrol masing-masing; semuanya dihilangkan di sini agar keputusan redirect tetap terlihat. Key yang tidak dikenal kembali ke path yang sudah diketahui. Client tidak pernah memasok byte yang ditempatkan di Location.
Pola ini juga tetap rapi ketika route berubah. Identifier publik dapat tetap berupa security sementara path di sisi server berubah. Dalam aplikasi yang lebih besar, named route dapat memberikan pemisahan yang sama tanpa memelihara map kedua di setiap controller.
Saat perpindahan ke layanan eksternal memang diperlukan
Beberapa aplikasi memang harus meninggalkan origin-nya: dokumentasi, penyedia pembayaran, atau layanan lain bisa menjadi tujuan yang sah. Pemetaan tetap masih bekerja ketika pilihannya sedikit:
<?php
$externalDestinations = [
'documentation' => 'https://docs.example.org/start',
'status' => 'https://status.example.org/',
];
$key = $_GET['to'] ?? '';
if (!array_key_exists($key, $externalDestinations)) {
http_response_code(400);
exit('Unknown destination');
}
header('Location: ' . $externalDestinations[$key], true, 302);
exit;
Jika tujuan harus dapat dikonfigurasi alih-alih ditetapkan dalam kode, keputusan kepercayaan berpindah ke workflow konfigurasi tersebut. Batasi siapa yang boleh menambah tujuan, tinjau perubahan, dan simpan nilai yang sudah diparse serta dikanonisasi, bukan menganggap setiap string yang dikirim sebagai sesuatu yang disetujui.
Untuk kumpulan yang lebih besar tetapi tetap terkendali, OWASP menyarankan parsing dengan parser terawat yang kompatibel dengan komponen yang akan memakai URL, kemudian membandingkan scheme, canonical host, effective port, dan batasan path yang diperlukan dengan allowlist eksplisit. Ini membutuhkan lebih banyak pekerjaan daripada memeriksa string hostname. Pendekatan tersebut juga layak mendapat pengujian untuk input ambigu dan perbedaan parser. Halaman konfirmasi yang menampilkan tujuan eksternal dengan jelas dapat membuat perpindahan yang disengaja terlihat, tetapi itu adalah langkah tambahan untuk usability dan defense, bukan izin untuk melewati validasi.
Mengapa pemeriksaan string yang familier mudah rapuh
Pemeriksaan seperti str_starts_with($url, 'https://example.com') mengajukan pertanyaan tentang string, bukan tentang authority. Hostname yang lebih panjang, komponen user-information, slash yang tidak biasa, backslash, encoding, atau perbedaan parser dapat mengubah apa yang dianggap browser sebagai tujuan. Karena itu, OWASP memperingatkan agar tidak memakai raw prefix dan suffix matching.
parse_url() juga tidak mengubah input menjadi URL tepercaya. Manual PHP menyatakan bahwa fungsi tersebut memecah URL dan tidak ditujukan untuk memvalidasinya; URL parsial dan tidak valid dapat diterima. Dokumentasi itu juga memperingatkan bahwa validasi dengan satu parser dan penggunaan dengan parser lain dapat menimbulkan masalah keamanan. WHATWG URL Standard dibuat antara lain agar parsing URL browser dapat interoperabel, dan bagian keamanannya mengingatkan bahwa URL yang diteruskan antarpihak dapat berasal dari sumber tidak tepercaya.
Parsing tetap berguna saat URL eksternal tidak dapat dihindari. Parsing membuka komponen yang dapat dibandingkan dengan kebijakan. Namun, "parser menghasilkan array" hanyalah pengamatan sintaksis. Aplikasi tetap harus memutuskan scheme, host, port, path, dan credential mana yang dapat diterima. Pada versi PHP yang menyediakan class URI sesuai standar, class tersebut dapat mengurangi ambiguitas parser, tetapi tetap tidak menciptakan allowlist aplikasi.
Pisahkan mekanisme HTTP dari keputusan kepercayaan
Menurut RFC 9110, Location membawa URI reference. Status code menjelaskan hubungan respons dengan request berikutnya. Dokumentasi header() PHP menyatakan bahwa field Location secara default menghasilkan 302 kecuali status 201 atau status 3xx lain sudah ditetapkan.
Lebih jelas jika status dipilih secara sengaja. Dalam contoh POST di atas, 303 berarti resource yang dipilih dapat diambil dengan GET atau HEAD, sesuai untuk tampilan setelah tindakan. Redirect sementara dari endpoint GET dapat memakai 302. Pemilihan status tidak memperbaiki tujuan yang tidak tepercaya: 303 yang dipilih dengan cermat menuju URL yang dikendalikan penyerang tetap merupakan open redirect.
PHP harus mengirim header sebelum output respons. Setelah menjadwalkan redirect, eksekusi juga terus berjalan kecuali program berhenti atau kembali melalui response framework. Memanggil exit tepat setelah redirect header() sederhana mencegah kode berikutnya berjalan tanpa sengaja. Ini adalah disiplin control flow, terpisah dari validasi tujuan.
Tinjau seluruh jalur redirect
Review yang berguna dimulai dari redirect sink lalu menelusurinya ke belakang. Cari Location:, helper redirect framework, dan navigasi client-side seperti location.assign(). Kode client-side dapat mengulang kesalahan trust boundary yang sama, meskipun source dan sink berada di browser, bukan di PHP.
Untuk setiap redirect, tanyakan:
- Apakah request field, cookie, nilai pengguna yang disimpan, header, atau respons eksternal dapat memengaruhi tujuan?
- Apakah fitur benar-benar membutuhkan URL, atau named destination sudah cukup?
- Jika URL eksternal diperlukan, kombinasi scheme, host, port, dan path mana yang secara eksplisit disetujui?
- Apakah validasi dan navigasi memakai interpretasi URL yang kompatibel?
- Apakah nilai yang tidak valid atau hilang gagal secara tertutup ke halaman lokal yang diketahui atau sebuah error?
- Apakah setiap tujuan menerapkan authorization secara mandiri?
- Apakah status code dipilih dengan sengaja, dan apakah eksekusi berhenti setelah respons ditetapkan?
Pengujian sebaiknya mencakup setiap identifier yang disetujui, identifier tidak dikenal, nilai kosong, bentuk lokal dan eksternal, hostname menyesatkan, sintaks user-information, network-path reference yang diawali //, backslash, karakter ter-encode, dan input terlalu panjang. Gunakan domain contoh yang dicadangkan dalam pengujian dan hanya uji sistem yang memang boleh kamu nilai. Hasil yang diharapkan harus eksplisit: tujuan yang disetujui melakukan redirect persis sesuai desain; semua yang lain menghasilkan error atau fallback yang diketahui.
Keputusan yang lebih kecil lebih mudah diamankan
Keamanan redirect menjadi sulit ketika aplikasi bertanya, "Apakah string sewenang-wenang ini aman di mana-mana?" Pertanyaannya jauh lebih mudah dikelola ketika berubah menjadi, "Apakah ini salah satu tujuan yang memang didukung alur ini?" Map di sisi server menjawab pertanyaan yang lebih kecil itu secara langsung.
Tidak semua redirect dinamis harus dihapus, dan parsing tetap diperlukan untuk sebagian integrasi. Batas pentingnya adalah data tidak tepercaya tidak boleh diam-diam berubah menjadi wewenang navigasi. Pilih tujuan yang sudah diketahui terlebih dahulu, gunakan allowlist berbasis komponen hanya ketika fitur benar-benar membutuhkannya, dan pertahankan authorization di tujuan. Desain tersebut tidak bergantung pada kemampuan mengenali setiap ejaan cerdik dari URL yang tidak diinginkan.
