Keamanan Siber

Validasi Host Header di PHP - Perlakukan Request Authority sebagai Input, Bukan Konfigurasi

Validasi Host Header di PHP - Perlakukan Request Authority sebagai Input, Bukan Konfigurasi

Aplikasi PHP kecil terkadang perlu mengetahui alamat publiknya sendiri. Aplikasi mungkin perlu membuat link reset password, menghasilkan canonical URL, atau mengalihkan pengunjung ke versi HTTPS suatu halaman. Jalan pintas yang praktis adalah menambahkan scheme di depan $_SERVER['HTTP_HOST']. Namun, siapa yang memilih host tersebut: operator aplikasi, reverse proxy, atau client yang membuat request saat itu?

Perbedaan itu penting karena sebuah request header bisa diam-diam berubah menjadi konfigurasi aplikasi. Field Host memang memiliki tugas yang sah dalam protokol, tetapi "ada dan valid secara sintaksis" tidak berarti "diizinkan untuk aplikasi ini." Pertahanan yang sederhana adalah memisahkan tiga keputusan: virtual host mana yang menerima request, authority mana yang diterima aplikasi, dan origin terkonfigurasi mana yang digunakan aplikasi ketika membuat link publik.

Apa yang diidentifikasi Host

Sebuah URL HTTP memiliki authority: biasanya hostname dan, bila diperlukan, port. Pada HTTP/1.1, field Host membawa informasi tersebut untuk request target berbentuk origin-form yang umum. HTTP/2 dan HTTP/3 biasanya menyatakan peran yang sama melalui pseudo-header :authority. RFC 9110 bagian 7.2 menjelaskan informasi authority ini sebagai bagian dari routing request dan identifikasi resource tujuannya.

Inilah yang memungkinkan beberapa situs berbagi satu alamat. Web server dapat menerima traffic pada satu socket lalu menggunakan nama yang diminta untuk memilih virtual host. Dokumentasi pemrosesan request Nginx menjelaskan bahwa Nginx terlebih dahulu mempersempit pilihan berdasarkan listen address dan port, kemudian membandingkan nama dalam request dengan server_name. Jika tidak ada nama yang cocok, default server untuk listen socket tersebut akan menangani request.

HTTP/1.1 juga menetapkan aturan dasar untuk message. RFC 9112 bagian 3.2 mewajibkan client mengirim Host, dan mewajibkan server mengembalikan 400 Bad Request ketika field tersebut tidak ada, muncul lebih dari sekali, atau memiliki nilai yang tidak valid. Pemeriksaan itu menjawab persoalan sintaks dan framing. Pemeriksaan tersebut tidak menjawab pertanyaan deployment: apakah preview.example.com, example.com:8443, atau domain valid lain memang nama yang hendak dilayani aplikasi ini?

Bagaimana nilai routing menjadi masalah keamanan

Risiko dimulai ketika code memperlakukan request authority sebagai sumber kebenaran tepercaya. PHP mengekspos request header melalui server variable; manual PHP untuk $_SERVER mencatat bahwa entry dibuat oleh web server dan request header mendapatkan nama dengan awalan HTTP_. Variable yang tersedia secara tepat bergantung pada interface server, tetapi HTTP_HOST tetap harus diperlakukan sebagai input request, bukan sebagai rahasia deployment.

Perhatikan endpoint reset yang membuat link berikut:

$resetUrl = 'https://' . $_SERVER['HTTP_HOST']
    . '/reset?token=' . rawurlencode($token);

Jika infrastruktur di sekitarnya menerima authority yang tidak diharapkan dan aplikasi mengirim URL tersebut melalui email, penerima bisa memperoleh token valid di dalam link yang mengarah ke origin yang salah. Pola ini lazim disebut password-reset poisoning. Pola tersebut tidak otomatis dapat dieksploitasi pada setiap deployment: routing, penulisan ulang oleh proxy, perilaku email, masa berlaku token, dan kontrol lain turut menentukan. Meski demikian, aplikasi telah membiarkan satu request memilih tujuan sebuah link sensitif tanpa perlu.

Kesalahan kepercayaan yang sama dapat memengaruhi absolute redirect, canonical link, cache entry, atau routing ke back-end. Panduan Host header dari PortSwigger mendokumentasikan berbagai konsekuensi yang mungkin terjadi dan menyarankan agar host dari request tidak digunakan untuk absolute URL bila memungkinkan. Pelajaran pentingnya lebih sempit daripada "Host header itu berbahaya": authority adalah input yang berguna untuk routing, tetapi bukan otomatis konfigurasi yang aman untuk setiap keputusan berikutnya.

Pisahkan tiga keputusan

1. Biarkan edge hanya me-route nama yang dipahaminya

Konfigurasikan default virtual server secara eksplisit agar request tidak jatuh ke aplikasi. Bentuk minimal Nginx adalah:

server {
    listen 80 default_server;
    server_name _;
    return 400;
}

server {
    listen 80;
    server_name example.com www.example.com;

    # Normal application locations go here.
}

Listener TLS dan IPv6 yang sebenarnya harus ditangani secara terpisah dalam konfigurasi nyata. Underscore di sini hanya nama non-domain yang digunakan sebagai konvensi; default_server yang menentukan fallback untuk listen socket. Mengembalikan 400 juga merupakan pilihan kebijakan lokal, bukan klaim bahwa setiap authority yang tidak cocok tetapi valid secara sintaksis melanggar HTTP.

2. Minta aplikasi menegakkan himpunan yang diterimanya sendiri

Filter pada edge berguna, tetapi aplikasi mungkin juga dapat dijangkau melalui listener lain, development server, atau rute proxy yang berubah. Untuk deployment kecil dengan sekumpulan nama domain ASCII yang tetap, perbandingan exact lebih mudah diaudit daripada parser yang permisif:

<?php

$allowedAuthorities = [
    'example.com',
    'www.example.com',
];

$authority = strtolower($_SERVER['HTTP_HOST'] ?? '');

if (!in_array($authority, $allowedAuthorities, true)) {
    http_response_code(400);
    header('Content-Type: text/plain; charset=utf-8');
    exit('Unrecognized request authority.');
}

Guard ini sengaja hanya melakukan sedikit normalisasi. Ia menerima perbedaan huruf besar-kecil karena hostname DNS tidak case-sensitive, lalu menolak semua yang tidak tercantum secara eksplisit. Port yang tidak diharapkan, trailing dot, suffix seperti example.com.attacker.test, whitespace, IP literal, atau alias development akan gagal kecuali deployment sengaja menambahkan bentuk persis tersebut.

Perilaku sempit itu bukan parser authority universal. Internationalized domain name, IPv6 literal, domain tenant yang dinamis, serta aplikasi yang sengaja melayani beberapa port membutuhkan kebijakan canonicalization dan onboarding yang terdokumentasi. Framework mungkin sudah menyediakannya. Sebagai contoh, dokumentasi ALLOWED_HOSTS Django menetapkan perilakunya sendiri untuk matching, port, wildcard, dan trailing dot. Contoh PHP ini tidak seharusnya menggantikan mekanisme validasi host milik framework yang telah ditinjau.

3. Buat URL publik dari konfigurasi

Validasi menjawab apakah sebuah request boleh dilanjutkan. Pembuatan canonical URL menjawab pertanyaan lain. Jika situs memiliki satu public origin, konfigurasikan satu kali:

$publicOrigin = rtrim((string) getenv('APP_PUBLIC_ORIGIN'), '/');

if ($publicOrigin !== 'https://example.com') {
    throw new RuntimeException('APP_PUBLIC_ORIGIN is not configured safely.');
}

$resetUrl = $publicOrigin
    . '/reset?token=' . rawurlencode($token);

Pemeriksaan literal tersebut merupakan ilustrasi untuk satu production origin yang tetap. Aplikasi nyata dapat memvalidasi konfigurasi saat startup. Sifat pentingnya adalah incoming request tidak memilih origin untuk email, webhook callback, canonical tag, atau absolute URL publik lainnya. Relative link bahkan lebih sederhana ketika konsumen tidak membutuhkan absolute URL.

Reverse proxy mengubah sumber, bukan kebutuhan untuk percaya

Proxy mungkin menerima public.example dan terhubung ke upstream bernama php.internal. Proxy dapat mempertahankan authority asli dalam Host, atau menaruhnya dalam Forwarded maupun X-Forwarded-Host. Tidak satu pun nama tersebut otomatis menjadi tepercaya hanya karena mengandung kata "forwarded."

Aplikasi hanya seharusnya memakai forwarded authority ketika direct peer merupakan proxy yang dikenal dan proxy tersebut menghapus setiap salinan dari client sebelum menulis nilainya sendiri. Jika tidak, client dapat menyediakan sendiri metadata yang seolah-olah berasal dari proxy. Ini merupakan prinsip boundary yang sama dengan forwarded client address, tetapi data dan konsekuensinya berbeda: keputusan di sini menyangkut public authority mana yang diwakili request.

Tidak ada satu aturan header yang cocok untuk setiap proxy chain. Dokumentasikan komponen mana yang memiliki public authority, cara setiap hop menulis ulang nilainya, dan nilai mana yang diterima aplikasi terakhir. Jika aplikasi hanya membutuhkan satu canonical public origin, konfigurasi tetap sering kali menghindarkan generated link dari persoalan ini sepenuhnya.

Uji jalur penolakan, bukan hanya happy path

Pada sistem yang kamu miliki atau berwenang kamu uji, kirim request dengan authority yang diharapkan serta variasi yang sengaja tidak diharapkan. Periksa edge dan aplikasi secara terpisah. Kasus yang berguna mencakup:

  • production hostname dengan variasi huruf besar-kecil biasa;
  • hostname yang tidak berkaitan;
  • suffix trick seperti example.com.attacker.test;
  • port yang tidak dikonfigurasi atau trailing dot;
  • X-Forwarded-Host yang diberikan langsung oleh client;
  • redirect, email reset, canonical tag, dan setiap response yang memuat absolute URL.

Authority yang ditolak tidak seharusnya dialihkan ke URL yang dibuat dari authority yang sama. Itu hanya memantulkan input tidak tepercaya ke response lain. Periksa juga perilaku cache dan log, sambil menghindari kesalahan menyimpan token reset sensitif dalam output diagnostik.

Apa yang tidak dibuktikan pertahanan ini

Allowlist tidak membuktikan bahwa DNS mengarah ke tujuan yang diharapkan, sertifikat TLS valid, proxy terisolasi dengan benar, atau cache membuat key response secara aman. Allowlist juga tidak memperbaiki reset token yang lemah atau output encoding yang tidak aman. Sebaliknya, TLS yang benar belum tentu menghentikan aplikasi membuat absolute link dari nilai request yang dimanipulasi setelah traffic mencapai endpoint yang diizinkan.

Platform multi-tenant juga tidak dapat mengganti proses kepemilikan domain dengan wildcard luas lalu menganggap masalah selesai. Platform tersebut membutuhkan pemetaan tepercaya dari tenant ke domain terverifikasi, beserta aturan aktivasi, penghapusan, dan canonicalization. Contoh exact-list berguna justru karena cakupannya lebih kecil.

Kesimpulan

Field Host bukan detail HTTP yang muncul tanpa tujuan. Field tersebut membantu server mengidentifikasi authority yang ingin dijangkau client. Kesalahannya adalah meminta nilai request itu menjalankan peran lain: konfigurasi aplikasi permanen.

Untuk situs PHP kecil, susunan yang dapat dipertanggungjawabkan cukup jelas: tolak nama yang tidak dikenal pada edge, tegakkan himpunan yang sempit di aplikasi, percayai forwarded authority hanya melintasi proxy boundary yang eksplisit, dan buat public absolute URL dari konfigurasi. Tidak ada satu pun lapisan itu yang ajaib. Jika digabungkan, semuanya membuat pertanyaan penting menjadi terlihat: komponen mana yang diizinkan memberi nama pada aplikasi ini?

References