Pencegahan SSRF pada Aplikasi PHP Kecil - URL yang Valid Bukan Berarti Tujuannya Aman
Aplikasi PHP kecil mungkin perlu mengimpor gambar, menampilkan preview tautan, menguji endpoint webhook, atau mengambil feed dari luar. Fiturnya terdengar biasa: terima URL, kirim request, lalu proses response. Namun, akses jaringan siapa yang dipakai? Request itu keluar dari server, bukan dari browser pengunjung, sehingga mungkin dapat menjangkau tempat yang tidak bisa dijangkau pengunjung.
Inilah batas yang melatarbelakangi server-side request forgery, atau SSRF. Masalahnya bukan sekadar input yang “terlihat seperti URL”. Masalah muncul ketika input yang tidak tepercaya dapat memengaruhi network client tanpa keputusan yang andal tentang tujuan mana yang boleh dihubungi. URL yang secara sintaks valid tetap dapat menunjuk ke layanan loopback, alamat private, endpoint link-local, protokol yang tidak diharapkan, atau host publik yang melakukan redirect ke tempat lain.
Artikel ini menyusun model review defensif untuk aplikasi PHP kecil. Isinya tidak memberikan payload serangan atau mengklaim satu fungsi validasi dapat membuat pengambilan URL arbitrer menjadi aman. Tujuan yang lebih berguna adalah mengenali setiap keputusan antara menerima sebuah string dan membuka koneksi.
SSRF Meminjam Posisi Server
CWE-918 dari MITRE menjelaskan SSRF sebagai kondisi ketika server mengambil URL atau request serupa tanpa memastikan secara memadai bahwa request tersebut menuju tujuan yang diharapkan. Susunan kata ini penting. Server mungkin berada di balik firewall, berbagi host dengan layanan administratif, memiliki akses ke private DNS, atau membawa credential yang ditujukan untuk satu upstream API. Request yang dibuat melalui server tersebut mewarisi sebagian posisi ini.
Dampaknya tidak terbatas pada membaca halaman web publik. OWASP SSRF Prevention Cheat Sheet mencatat bahwa pengambilan dari sisi server dapat melibatkan jaringan internal atau eksternal, mesin lokal, dan protokol selain HTTP. Dampak tepatnya bergantung pada apa yang dapat dijangkau proses dan bagaimana response boleh memengaruhi sistem.
Fitur-fitur umum dapat menciptakan batas ini tanpa terlihat seperti fitur keamanan:
- mengunduh avatar atau gambar artikel dari URL yang dikirim pengguna;
- membuat judul dan thumbnail untuk preview tautan;
- memungkinkan pengguna menguji tujuan webhook;
- mengimpor kalender, feed, atau dokumen;
- meminta tool otomatisasi mengambil URL yang ditemukan dalam konten eksternal.
Karena itu, pertanyaan desain pertama bukan “Validator URL mana yang harus dipakai?” Pertanyaan pertamanya adalah “Apakah fitur ini memang perlu terhubung ke tujuan yang dipilih pengguna?”
URL yang Valid Bukan Berarti Tujuannya Diizinkan
Sintaks URL dan otorisasi jaringan menjawab dua pertanyaan yang berbeda. Validasi sintaks mungkin memastikan bahwa sebuah string sesuai dengan aturan parser. Validasi itu tidak memastikan bahwa host tersebut tepercaya, alamatnya saat ini dapat dijangkau secara global, port-nya sesuai, atau tujuannya akan tetap sama setelah redirect.
Di PHP, perbedaan ini sangat mudah terlewat. Dokumentasi resmi parse_url() menyatakan secara langsung bahwa fungsi tersebut bukan validator URL. Fungsi ini dapat menerima URL parsial atau tidak valid, tidak mengikuti satu standar URL tertentu, dan mungkin menafsirkan input secara berbeda dari parser lain. Poin terakhir sangat penting ketika satu komponen memeriksa host, tetapi libcurl atau proxy kemudian menafsirkan request yang sebenarnya.
FILTER_VALIDATE_URL memang merupakan validation filter, tetapi lolos dari filter itu tetap tidak menerapkan kebijakan tujuan milik aplikasi. Dokumentasi filter PHP menjelaskan apakah data lolos dari filter yang dipilih; dokumentasi itu tidak menjanjikan bahwa tujuan hasilnya aman untuk dihubungi server. Menganggap validitas sintaks sebagai otorisasi seperti memeriksa apakah alamat ditulis dengan format pos yang benar, lalu menyimpulkan semua bangunan di alamat itu terbuka untuk pengunjung.
URL juga memiliki struktur yang lebih banyak daripada hostname. RFC 3986 memisahkan scheme, authority, path, query, dan fragment, dengan informasi pengguna, host, dan port berada di dalam authority. Reserved character dan percent-encoding memengaruhi penafsiran. Keputusan keamanan sebaiknya memakai parser yang terdefinisi jelas dan membandingkan komponen hasil parsing, bukan mencari substring tepercaya di dalam string asli.
URL Teraman Sering Kali Adalah URL yang Tidak Pernah Diberikan Pengguna
Jika aplikasi hanya berkomunikasi dengan API yang sudah diketahui, jangan menerima URL lengkap. Terima business identifier yang sempit, seperti resource ID, lalu bangun request dari base URL milik aplikasi, scheme tetap, port tetap, dan path encoding yang dikendalikan. Pendekatan ini menghilangkan jauh lebih banyak ambiguitas daripada mencoba menolak setiap bentuk berbahaya dari URL umum.
Jika ada beberapa tujuan yang sah, allowlist dapat berisi host atau service identifier yang tepat. Pencocokan harus dilakukan terhadap host yang telah di-parse dan dinormalisasi berdasarkan satu kebijakan yang terdokumentasi. Pemeriksaan suffix seperti “diakhiri dengan example.com” tidak setara dengan memilih host yang tepat, yaitu api.example.com. Port dan scheme juga memerlukan allowlist tersendiri.
OWASP membedakan kasus tujuan yang sudah diketahui ini dari kasus yang jauh lebih sulit, yaitu ketika produk benar-benar mengizinkan tujuan publik arbitrer. Ini adalah perbedaan produk yang penting. Integrasi internal, webhook tester, dan layanan preview tautan terbuka tidak memiliki kebijakan tujuan yang sama. Jika requirement dapat dipersempit, mempersempitnya adalah kontrol keamanan, bukan sekadar ketidaknyamanan.
Ketika URL Publik Arbitrer Memang Diperlukan
Sebagian fitur tidak dapat memakai allowlist tujuan yang kecil. Fitur semacam itu memerlukan pipeline yang lebih hati-hati, dan setiap kegagalan harus menolak request sebelum koneksi dibuka.
1. Parse Sekali dengan Kontrak yang Eksplisit
Pilih model parser yang dipakai runtime dan retrieval stack, dokumentasikan, lalu tolak input malformed atau ambigu alih-alih memperbaikinya secara diam-diam. Wajibkan URL absolut, host yang jelas, dan tidak adanya komponen informasi pengguna. Izinkan hanya scheme yang diperlukan fitur, biasanya https dan mungkin http jika ada alasan yang terdokumentasi. Izinkan hanya port yang diharapkan.
Pemeriksaan ini mengurangi ambiguitas input, tetapi belum mengotorisasi tujuan.
2. Resolve Setiap Address Family dan Klasifikasikan Semua Hasil
Hostname bukan endpoint yang dipakai koneksi jaringan. DNS mengubahnya menjadi satu atau beberapa alamat IPv4 atau IPv6, dan hasil tersebut dapat berubah. Aplikasi memerlukan kebijakan untuk setiap alamat yang dikembalikan, bukan hanya A record pertama yang kebetulan nyaman.
Hanya memeriksa rentang IPv4 RFC 1918 yang familier tidaklah cukup. Registry special-purpose IPv4 dan IPv6 dari IANA saat ini mencakup blok loopback, link-local, shared, documentation, benchmarking, unique-local, mapped, dan blok khusus lainnya dengan sifat reachability yang berbeda. Library alamat IP yang dipelihara dan pemeriksaan eksplisit “dapat dijangkau secara global menurut kebijakan aplikasi ini” lebih aman daripada daftar prefix buatan sendiri yang disalin ke satu controller.
Ada persoalan waktu yang sulit di sini. Jika kode aplikasi melakukan resolve terhadap hostname, menyetujui hasilnya, lalu memberikan hostname asli kepada komponen lain, komponen itu dapat melakukan lookup DNS baru dan menerima alamat yang berbeda. OWASP membahas kelas masalah DNS pinning atau rebinding ini. Menutup celah antara pemeriksaan dan koneksi dapat mengharuskan koneksi diikat ke alamat hasil resolve yang telah disetujui sambil tetap mempertahankan host tujuan untuk verifikasi HTTP dan TLS. Metode tepatnya bergantung pada HTTP client, DNS resolver, proxy, dan runtime. Karena itulah helper PHP universal yang singkat akan menyesatkan.
3. Perlakukan Setiap Redirect sebagai Request Baru
URL publik dapat mengembalikan redirect menuju tujuan yang seharusnya ditolak oleh pemeriksaan awal. Mengikuti redirect secara otomatis berarti memindahkan keputusan keamanan ke tahap setelah validasi, kecuali setiap target baru melewati pemeriksaan scheme, port, hostname, DNS, dan alamat yang sama.
libcurl membiarkan CURLOPT_FOLLOWLOCATION nonaktif secara default. Membiarkannya nonaktif adalah kebijakan paling sederhana ketika redirect tidak diperlukan. Jika fitur memerlukan redirect, gunakan batas hop yang kecil dan periksa setiap target sebelum melanjutkan. Jangan berasumsi pembatasan protokol redirect juga memvalidasi host tujuan redirect.
4. Batasi Protokol di Network Client
Aplikasi mungkin hanya bermaksud mengambil halaman web sementara build libcurl-nya mendukung banyak protokol lain. Dokumentasi CURLOPT_PROTOCOLS_STR dari proyek curl menyatakan bahwa default-nya adalah setiap protokol yang disertakan dalam build libcurl. Daftar protokol yang eksplisit membuat perilaku client sesuai dengan kontrak fitur.
Redirect memiliki kontrol terpisah, yaitu CURLOPT_REDIR_PROTOCOLS_STR. Ini adalah defense in depth, bukan pengganti pemeriksaan tujuan pada setiap hop. Kompatibilitas runtime juga penting: opsi berbasis string tersebut ditambahkan pada libcurl 7.85.0, sehingga versi PHP dan libcurl yang dipasang harus diperiksa sebelum mengandalkannya.
Tempatkan Batas Kedua di Jaringan
Validasi aplikasi adalah kode terperinci yang berhadapan dengan banyak edge case. Validasi tersebut sebaiknya tidak menjadi satu-satunya penghalang antara worker pengambil URL dan setiap layanan di host atau jaringan lokal. OWASP menyarankan penggabungan kontrol di lapisan aplikasi dan jaringan.
Di server kecil, ini dapat berarti menjalankan proses pengambilan URL dengan service identity atau worker terisolasi, lalu menerapkan outbound firewall rule, proxy terkendali, atau kebijakan network namespace. Worker hanya boleh menjangkau tujuan dan port yang dibutuhkan tugasnya, sementara interface administratif, port database, control socket lokal, dan metadata service tetap tidak dapat dijangkau dari konteks tersebut.
Pembatasan egress tidak otomatis sederhana. Tujuan publik dapat memakai rentang alamat yang berubah, DNS mungkin ditangani proxy, dan host yang sama mungkin menjalankan beberapa layanan dengan kebutuhan berbeda. Kebijakan harus sesuai dengan jalur koneksi yang benar-benar dipakai. Meski demikian, batas jaringan yang independen dapat mengubah satu kesalahan parser dari akses server tanpa batas menjadi koneksi yang ditolak.
Batasi Proses Fetch Bahkan Setelah Tujuan Disetujui
Pemeriksaan tujuan menjawab ke mana request boleh pergi. Pemeriksaan itu tidak mengendalikan seberapa mahal atau berbahaya response yang diterima. Proses fetch yang terbatas juga perlu menentukan:
- batas waktu koneksi dan total;
- jumlah maksimum redirect, sebaiknya nol kecuali memang diperlukan;
- ukuran maksimum response yang ditegakkan saat streaming, bukan hanya setelah buffering;
- content type yang diterima dan parser aman untuk format yang diharapkan;
- batas decompression serta pemrosesan gambar atau dokumen setelahnya;
- apakah response body disimpan, ditampilkan, atau dibuang.
Jangan teruskan ambient credential, cookie, atau header inbound arbitrer ke host yang dipilih pengguna. libcurl mendokumentasikan perlakuan khusus untuk authorization header dan explicit cookie header lintas host, tetapi juga memperingatkan bahwa custom header lain mungkin berisi data yang tidak seharusnya dikirim ke server hasil redirect. Bangun outbound request dari kumpulan header minimal.
Response yang diambil tidak boleh langsung dipantulkan ke halaman HTML atau diperlakukan sebagai data aplikasi tepercaya. Pencegahan SSRF mengendalikan tujuan request; output encoding, validasi file, parser hardening, dan kebijakan malware adalah batas yang berbeda.
Review Seluruh Jalur Keputusan
Review yang berguna mengikuti aliran nilai, bukan hanya satu pemanggilan fungsi:
- Identifikasi setiap tempat di mana data tidak tepercaya dapat memengaruhi request dari sisi server.
- Tanyakan apakah business identifier atau tujuan tetap dapat menggantikan URL lengkap.
- Tentukan scheme, tujuan tepat atau kebijakan alamat publik, port, dan perilaku redirect yang diizinkan.
- Pastikan validasi dan retrieval sepakat tentang penafsiran URL.
- Periksa semua hasil resolusi IPv4 dan IPv6, lalu tutup celah antara persetujuan dan koneksi.
- Jalankan kembali kebijakan untuk setiap redirect.
- Batasi protokol client dan tambahkan batas egress yang independen.
- Batasi waktu, ukuran data, redirect, parsing, dan data yang disimpan.
- Catat hasil kebijakan, kelas tujuan, durasi, dan alasan kegagalan tanpa merekam credential atau response body sensitif.
Pengujian sebaiknya mencakup IP literal langsung, IPv4 dan IPv6, beberapa jawaban DNS, port dan scheme yang dilarang, redirect ke tujuan yang ditolak, perubahan resolver di antara pemeriksaan, response berukuran besar dan terkompresi, timeout, serta deployment yang memakai proxy. Ini adalah pengujian kebijakan aplikasi sendiri, bukan klaim bahwa kumpulan pengujian terbatas dapat mencakup setiap penafsiran URL.
Kesimpulan
Pencegahan SSRF dimulai dengan menyadari bahwa fetch dari sisi server adalah keputusan tentang kewenangan. Sebuah URL dapat memiliki sintaks yang sepenuhnya valid, tetapi tetap meminta server menyeberangi batas yang tidak dapat dilewati pengguna secara langsung.
Desain aplikasi kecil yang paling kuat menghindari URL arbitrer dan menyusun request menuju tujuan yang sudah diketahui. Jika pengambilan dari internet terbuka memang diperlukan, tidak ada satu parser, filter, atau denylist yang cukup. Parsing, resolusi alamat, penanganan redirect, pembatasan protokol, batas resource, dan kontrol egress jaringan harus sepakat pada kebijakan yang sama.
Jawaban berlapis ini memang kurang memuaskan dibanding validator satu baris, tetapi lebih jujur. Pertanyaan terakhir untuk fitur pengambil URL bukan hanya “Apakah URL ini valid?” Pertanyaannya adalah “Pada saat setiap koneksi dibuat, bukti apa yang menyatakan proses ini boleh pergi ke sana?”
References
- MITRE CWE, “CWE-918: Server-Side Request Forgery (SSRF),” CWE 4.20, updated April 30, 2026.
- OWASP Cheat Sheet Series, “Server-Side Request Forgery Prevention Cheat Sheet”.
- T. Berners-Lee, R. Fielding, and L. Masinter, RFC 3986: Uniform Resource Identifier (URI): Generic Syntax, January 2005.
- PHP Documentation Group, “parse_url”.
- PHP Documentation Group, “filter_var”.
- curl project, “CURLOPT_PROTOCOLS_STR explained”.
- curl project, “CURLOPT_REDIR_PROTOCOLS_STR explained”.
- curl project, “CURLOPT_FOLLOWLOCATION explained”.
- IANA, “IPv4 Special-Purpose Address Space,” updated October 9, 2025.
- IANA, “IPv6 Special-Purpose Address Space,” updated October 9, 2025.
