PHP $ _SERVER ['HTTP_HOST'] vs. $ _SERVER ['SERVER_NAME'], apakah saya memahami halaman manual dengan benar?

167

Saya melakukan banyak pencarian dan juga membaca PHP $ _SERVER docs . Apakah saya memiliki hak ini yang akan digunakan untuk skrip PHP saya untuk definisi tautan sederhana yang digunakan di seluruh situs saya?

$_SERVER['SERVER_NAME'] didasarkan pada file konfigurasi server web Anda (Apache2 dalam kasus saya), dan bervariasi tergantung pada beberapa arahan: (1) VirtualHost, (2) ServerName, (3) UseCanonicalName, dll.

$_SERVER['HTTP_HOST'] didasarkan pada permintaan dari klien.

Oleh karena itu, bagi saya tampaknya yang tepat untuk digunakan dalam rangka membuat skrip saya sel kompatibel mungkin $_SERVER['HTTP_HOST']. Apakah asumsi ini benar?

Komentar tindak lanjut:

Saya kira saya mendapat paranoid kecil setelah membaca artikel ini dan mencatat bahwa beberapa orang mengatakan "mereka tidak akan mempercayai salah satu $_SERVERvars":

Rupanya diskusi terutama tentang $_SERVER['PHP_SELF']dan mengapa Anda tidak harus menggunakannya dalam atribut form action tanpa melarikan diri untuk mencegah serangan XSS.

Kesimpulan saya tentang pertanyaan awal saya di atas adalah "aman" untuk digunakan $_SERVER['HTTP_HOST']untuk semua tautan di situs tanpa harus khawatir tentang serangan XSS, bahkan ketika digunakan dalam bentuk.

Harap perbaiki saya jika saya salah.

Jeff
sumber

Jawaban:

149

Itu mungkin pikiran pertama semua orang. Tapi ini sedikit lebih sulit. Lihat artikel Chris Shiflett SERVER_NAMEVersusHTTP_HOST .

Tampaknya tidak ada peluru perak. Hanya ketika Anda memaksa Apache untuk menggunakan nama kanonik Anda akan selalu mendapatkan nama server yang tepat SERVER_NAME.

Jadi Anda bisa menggunakan itu atau Anda memeriksa nama host terhadap daftar putih:

$allowed_hosts = array('foo.example.com', 'bar.example.com');
if (!isset($_SERVER['HTTP_HOST']) || !in_array($_SERVER['HTTP_HOST'], $allowed_hosts)) {
    header($_SERVER['SERVER_PROTOCOL'].' 400 Bad Request');
    exit;
}
Gumbo
sumber
4
Lol, saya membaca artikel itu dan sepertinya tidak benar-benar menjawab pertanyaan saya. Yang mana yang digunakan para profesional pengembang? Jika salah satunya.
Jeff
2
Sangat menarik, saya tidak pernah tahu SERVER_NAME menggunakan nilai yang disediakan pengguna secara default di Apache.
Powerlord
1
@ Jeff, Untuk server yang meng-host lebih dari satu sub / domain, Anda hanya memiliki dua pilihan $_SERVER['SERVER_NAME']dan $_SERVER['HTTP_HOST'](selain menerapkan beberapa jabat tangan kustom lain berdasarkan permintaan pengguna). Para profesional tidak percaya pada hal-hal yang tidak mereka mengerti sepenuhnya. Jadi mereka memiliki pengaturan SAPI mereka dengan sempurna dengan benar (dalam hal ini opsi yang mereka gunakan akan memberikan hasil yang benar), atau mereka akan melakukan daftar putih sedemikian rupa sehingga tidak masalah apa nilai yang diberikan oleh pasokan SAPI.
Pacerier
@ Gumbo, Anda perlu menerapkan tambalan "port" karena masalah serius dengan SAPI tertentu. Juga, array_key_existsadalah lebih terukur dibandingkan dengan in_arrayyang memiliki O (n) kinerja.
Pacerier
2
@Pacerier array_key_exists dan in_array melakukan hal-hal yang berbeda, mantan memeriksa kunci, nilai-nilai terakhir, sehingga Anda tidak bisa hanya menukar mereka. Juga, jika Anda memiliki array dua nilai, Anda seharusnya tidak benar-benar khawatir tentang kinerja O (n) ...
eis
74

Hanya catatan tambahan - jika server berjalan pada port selain 80 (seperti yang umum pada mesin pengembangan / intranet) maka HTTP_HOSTberisi port, sementara SERVER_NAMEtidak.

$_SERVER['HTTP_HOST'] == 'localhost:8080'
$_SERVER['SERVER_NAME'] == 'localhost'

(Setidaknya itulah yang saya perhatikan di virtualhosts berbasis port Apache)

Seperti Mike telah mencatat di bawah ini, HTTP_HOSTtidak tidak mengandung :443ketika berjalan di HTTPS (kecuali jika Anda berjalan pada port non-standar, yang saya belum diuji).

Simon Timur
sumber
4
Catatan: Port tidak ada dalam HTTP_HOST untuk 443 (port SSL default).
Mike
Jadi dengan kata lain, nilai HTTP_HOSTbukan Host:parameter yang diberikan pengguna. Itu hanya berdasarkan itu.
Pacerier
1
@Pacerier Tidak, ini kebalikannya: HTTP_HOST adalah persis Host: bidang yang disertakan dengan permintaan HTTP. Port adalah bagian dari itu dan browser tidak menyebutkannya ketika itu adalah default (80 untuk HTTP; 443 untuk HTTPS)
xhienne
29

Gunakan salah satunya. Keduanya sama-sama aman (dalam), seperti dalam banyak kasus SERVER_NAME hanya diisi dari HTTP_HOST. Saya biasanya menggunakan HTTP_HOST, sehingga pengguna tetap menggunakan nama host yang tepat. Misalnya, jika saya memiliki situs yang sama di domain .com dan .org, saya tidak ingin mengirim seseorang dari .org ke .com, terutama jika mereka mungkin memiliki token masuk di .org yang akan hilang jika dikirim ke domain lainnya.

Either way, Anda hanya perlu memastikan bahwa aplikasi web Anda hanya akan pernah merespons untuk domain yang dikenal baik. Ini dapat dilakukan (a) dengan pemeriksaan sisi aplikasi seperti Gumbo, atau (b) dengan menggunakan host virtual pada nama domain yang Anda inginkan yang tidak menanggapi permintaan yang memberikan header Host yang tidak dikenal.

Alasan untuk ini adalah bahwa jika Anda mengizinkan situs Anda diakses dengan nama lama apa pun, Anda membuka diri terhadap serangan DNS rebinding (di mana nama host situs lain menunjuk ke IP Anda, pengguna mengakses situs Anda dengan nama host penyerang, maka nama host dipindahkan ke IP penyerang, bawa cookie Anda / auth dengan itu) dan pembajak mesin pencari (di mana seorang penyerang menunjukkan nama host mereka sendiri di situs Anda dan mencoba untuk membuat mesin pencari melihatnya sebagai nama host utama 'terbaik').

Rupanya diskusi terutama tentang $ _SERVER ['PHP_SELF'] dan mengapa Anda tidak boleh menggunakannya dalam atribut form action tanpa melarikan diri dengan benar untuk mencegah serangan XSS.

Pfft. Yah Anda tidak harus menggunakan apa pun di atribut pun tanpa melarikan diri htmlspecialchars($string, ENT_QUOTES), jadi tidak ada yang istimewa tentang variabel server di sana.

bobince
sumber
Tetap dengan solusi (a), (b) tidak benar-benar aman, menggunakan URI absolut dalam permintaan HTTP memungkinkan untuk bypass keamanan virtualhosts berbasis nama. Jadi aturan sebenarnya adalah jangan pernah percaya pada SERVER_NAME atau HTTP_HOST.
regilero
@obobince, Bagaimana cara mesin pencari pembajakan bekerja? Mesin pencari memetakan kata-kata ke url domain , mereka tidak berurusan dengan IP. Jadi mengapa Anda mengatakan bahwa "seorang penyerang dapat membuat mesin pencari melihat attacker.comsebagai sumber utama terbaik untuk IP server Anda" berarti? Tampaknya itu tidak berarti apa-apa untuk mesin pencari, Apa yang bahkan akan dilakukan?
Pacerier
2
Google tentu memiliki (dan mungkin masih dalam beberapa bentuk) konsep situs dupe, sehingga jika situs Anda dapat diakses http://example.com/, http://www.example.com/dan http://93.184.216.34/itu akan menggabungkan mereka ke dalam satu situs, pilih alamat yang paling populer, dan hanya kembalikan tautan ke sana Versi: kapan. Jika Anda bisa menunjuk evil-example.compada alamat yang sama dan membuat Google melihat secara singkat bahwa sebagai alamat yang lebih populer Anda bisa mencuri jus situs. Saya tidak tahu bagaimana praktisnya hari ini, tetapi saya pernah melihat penyerang pertanian Rusia mencoba melakukannya di masa lalu.
bobince
24

Ini adalah terjemahan verbose dari apa yang digunakan Symfony untuk mendapatkan nama host ( lihat contoh kedua untuk terjemahan yang lebih literal ):

function getHost() {
    $possibleHostSources = array('HTTP_X_FORWARDED_HOST', 'HTTP_HOST', 'SERVER_NAME', 'SERVER_ADDR');
    $sourceTransformations = array(
        "HTTP_X_FORWARDED_HOST" => function($value) {
            $elements = explode(',', $value);
            return trim(end($elements));
        }
    );
    $host = '';
    foreach ($possibleHostSources as $source)
    {
        if (!empty($host)) break;
        if (empty($_SERVER[$source])) continue;
        $host = $_SERVER[$source];
        if (array_key_exists($source, $sourceTransformations))
        {
            $host = $sourceTransformations[$source]($host);
        } 
    }

    // Remove port number from host
    $host = preg_replace('/:\d+$/', '', $host);

    return trim($host);
}

Usang:

Ini adalah terjemahan saya untuk menghapus PHP dari metode yang digunakan dalam kerangka kerja Symfony yang mencoba untuk mendapatkan nama host dari segala cara yang mungkin dalam urutan praktik terbaik:

function get_host() {
    if ($host = $_SERVER['HTTP_X_FORWARDED_HOST'])
    {
        $elements = explode(',', $host);

        $host = trim(end($elements));
    }
    else
    {
        if (!$host = $_SERVER['HTTP_HOST'])
        {
            if (!$host = $_SERVER['SERVER_NAME'])
            {
                $host = !empty($_SERVER['SERVER_ADDR']) ? $_SERVER['SERVER_ADDR'] : '';
            }
        }
    }

    // Remove port number from host
    $host = preg_replace('/:\d+$/', '', $host);

    return trim($host);
}
bersifat antitoksin
sumber
1
@StefanNch Silakan tentukan "jalan ini".
showdev
1
@showdev Saya benar-benar merasa "sulit" untuk membaca pernyataan kondisi seperti if ($host = $_SERVER['HTTP_X_FORWARDED_HOST'])atau x = a == 1 ? True : False. Pertama kali saya melihatnya, otak saya mencari instantiasi $ host dan jawaban untuk "mengapa hanya satu" = "tanda?". Saya mulai tidak menyukai bahasa pemrograman mengetik yang lemah. Semuanya ditulis berbeda. Anda tidak menghemat waktu dan Anda tidak istimewa. Saya tidak menulis kode dengan cara ini, karena setelah waktu berlalu, saya yang perlu men-debug-nya. Terlihat sangat berantakan untuk otak yang lelah! Saya tahu bahasa Inggris saya Inggris, tetapi setidaknya saya mencoba.
StefanNch
1
teman-teman, saya hanya porting kode dari Symfony. Ini adalah cara saya mengambilnya. Untuk semua itu penting - itu berfungsi dan tampaknya cukup menyeluruh. Saya sendiri, juga hal ini tidak cukup dibaca tetapi saya belum punya waktu untuk menulis ulang sepenuhnya.
antitoksik
2
Terlihat baik untuk saya. Mereka adalah operator ternary dan benar-benar dapat menghemat waktu (dan byte) tanpa mengurangi keterbacaan, ketika digunakan dengan tepat.
showdev
1
@ antitoxic, -1 Symfony coders (seperti yang lainnya) tidak tahu persis apa yang mereka lakukan dalam kasus ini. Ini tidak memberi Anda nama host (lihat jawaban Simon). Ini hanya memberi Anda tebakan terbaik yang akan salah berkali-kali.
Pacerier
11

Apakah "aman" untuk digunakan $_SERVER['HTTP_HOST']untuk semua tautan di situs tanpa harus khawatir tentang serangan XSS, bahkan ketika digunakan dalam bentuk?

Ya, aman untuk digunakan $_SERVER['HTTP_HOST'], (dan bahkan $_GETdan $_POST) selama Anda memverifikasinya sebelum menerimanya. Inilah yang saya lakukan untuk server produksi yang aman:

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
$reject_request = true;
if(array_key_exists('HTTP_HOST', $_SERVER)){
    $host_name = $_SERVER['HTTP_HOST'];
    // [ need to cater for `host:port` since some "buggy" SAPI(s) have been known to return the port too, see http://goo.gl/bFrbCO
    $strpos = strpos($host_name, ':');
    if($strpos !== false){
        $host_name = substr($host_name, $strpos);
    }
    // ]
    // [ for dynamic verification, replace this chunk with db/file/curl queries
    $reject_request = !array_key_exists($host_name, array(
        'a.com' => null,
        'a.a.com' => null,
        'b.com' => null,
        'b.b.com' => null
    ));
    // ]
}
if($reject_request){
    // log errors
    // display errors (optional)
    exit;
}
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
echo 'Hello World!';
// ...

Keuntungannya $_SERVER['HTTP_HOST']adalah bahwa perilakunya lebih jelas daripada $_SERVER['SERVER_NAME']. Kontras ➫➫ :

Isi Host: tajuk dari permintaan saat ini, jika ada.

dengan:

Nama host server tempat skrip saat ini dijalankan.

Menggunakan antarmuka yang didefinisikan lebih baik seperti $_SERVER['HTTP_HOST']berarti bahwa lebih banyak SAPI akan mengimplementasikannya menggunakan perilaku yang terdefinisi dengan baik dan dapat diandalkan . (Berbeda dengan yang lain .) Namun, itu masih sepenuhnya tergantung pada SAPI ➫➫ :

Tidak ada jaminan bahwa setiap server web akan menyediakan salah satu dari [ $_SERVERentri] ini; server dapat menghilangkan beberapa, atau menyediakan yang lain yang tidak tercantum di sini.

Untuk memahami cara mengambil nama host dengan benar, pertama dan terutama, Anda perlu memahami bahwa server yang hanya berisi kode tidak memiliki cara untuk mengetahui (prasyarat untuk memverifikasi) namanya sendiri di jaringan. Perlu antarmuka dengan komponen yang memasok namanya sendiri. Ini dapat dilakukan melalui:

  • file konfigurasi lokal

  • database lokal

  • kode sumber hardcoded

  • permintaan eksternal ( ikal )

  • Host:permintaan klien / penyerang

  • dll

Biasanya ini dilakukan melalui file konfigurasi lokal (SAPI). Perhatikan bahwa Anda telah mengonfigurasinya dengan benar, misalnya di Apache ➫➫ :

Beberapa hal perlu 'dipalsukan' untuk membuat host virtual dinamis terlihat seperti yang normal.

Yang paling penting adalah nama server yang digunakan oleh Apache untuk menghasilkan URL referensi-diri, dll. Ini dikonfigurasi dengan ServerNamearahan, dan tersedia untuk CGI melalui SERVER_NAMEvariabel lingkungan.

Nilai aktual yang digunakan pada saat dijalankan dikendalikan oleh pengaturan UseCanonicalName.

Dengan UseCanonicalName Off nama server berasal dari isi Host:header dalam permintaan. Dengan UseCanonicalName DNS itu berasal dari pencarian DNS terbalik dari alamat IP virtual host. Pengaturan sebelumnya digunakan untuk hosting virtual dinamis berbasis nama, dan yang terakhir digunakan untuk hosting berbasis IP **.

Jika Apache tidak dapat mengetahui nama server karena tidak ada Host:tajuk atau pencarian DNS gagal maka nilai yang dikonfigurasi dengan ServerNamedigunakan sebagai gantinya.

Pacerier
sumber
8

Perbedaan utama antara keduanya adalah $_SERVER['SERVER_NAME']variabel yang dikendalikan server, sedangkan $_SERVER['HTTP_HOST']nilai yang dikendalikan pengguna.

Aturan praktisnya adalah jangan pernah mempercayai nilai-nilai dari pengguna, jadi itu $_SERVER['SERVER_NAME']adalah pilihan yang lebih baik.

Seperti yang ditunjukkan Gumbo, Apache akan membuat SERVER_NAME dari nilai yang disediakan pengguna jika Anda tidak menyetel UseCanonicalName On.

Sunting: Setelah mengatakan semua itu, jika situs menggunakan host virtual berbasis nama, header HTTP Host adalah satu-satunya cara untuk mencapai situs yang bukan situs default.

Powerlord
sumber
Dimengerti Hangup saya adalah "bagaimana mungkin pengguna mengubah nilai $ _SERVER ['HTTP_HOST']?" Apakah itu mungkin?
Jeff
5
Seorang pengguna dapat mengubahnya karena itu hanya isi dari header Host dari permintaan yang masuk. Server utama (atau VirtualHost terikat ke default : 80) akan menanggapi semua host yang tidak dikenal, sehingga konten tag Host di situs itu dapat diatur ke apa saja.
Powerlord
4
Perhatikan bahwa host virtual berbasis IP akan SELALU merespons pada IP spesifik mereka, sehingga Anda tidak dapat dalam keadaan apa pun mempercayai nilai HTTP Host pada mereka.
Powerlord
1
@ Jeff, Ini seperti bertanya "Apakah mungkin untuk memanggil nomor telepon pizza hut dan meminta untuk berbicara dengan staf KFC?" Tentu saja Anda dapat meminta apa pun yang Anda inginkan. @ Powerlord, Ini tidak ada hubungannya dengan host virtual berbasis IP. Server Anda, terlepas dari host virtual berbasis IP atau tidak, dalam keadaan apa pun tidak dapat mempercayai nilai HTTP Host:kecuali Anda telah memverifikasinya , baik secara manual atau melalui pengaturan SAPI Anda.
Pacerier
3

Saya tidak yakin dan tidak benar-benar percaya $_SERVER['HTTP_HOST']karena itu tergantung pada header dari klien. Dengan cara lain, jika domain yang diminta oleh klien bukan milik saya, mereka tidak akan masuk ke situs saya karena DNS dan protokol TCP / IP mengarahkannya ke tujuan yang benar. Namun saya tidak tahu apakah mungkin untuk membajak DNS, jaringan atau bahkan server Apache. Agar aman, saya mendefinisikan nama host di lingkungan dan membandingkannya dengan$_SERVER['HTTP_HOST'] .

Tambahkan SetEnv MyHost domain.comfile .htaccess pada root dan tambahkan kode ths di Common.php

if (getenv('MyHost')!=$_SERVER['HTTP_HOST']) {
  header($_SERVER['SERVER_PROTOCOL'].' 400 Bad Request');
  exit();
}

Saya menyertakan file Common.php ini di setiap halaman php. Halaman ini melakukan apa saja yang diperlukan untuk setiap permintaan seperti session_start(), memodifikasi cookie sesi dan menolak jika metode posting berasal dari domain yang berbeda.

CallMeLaNN
sumber
1
Tentu saja dimungkinkan untuk mem-bypass DNS. Penyerang bisa mengeluarkan Host:nilai fradulen langsung ke IP server Anda.
Pacerier
1

XSSakan selalu ada bahkan jika Anda menggunakan $_SERVER['HTTP_HOST'], $_SERVER['SERVER_NAME']ATAU$_SERVER['PHP_SELF']

Jaydeep Dave
sumber
1

Pertama saya ingin mengucapkan terima kasih atas semua jawaban dan penjelasan yang baik. Ini adalah metode yang saya buat berdasarkan semua jawaban Anda untuk mendapatkan url dasar. Saya hanya menggunakannya dalam situasi yang sangat jarang. Jadi TIDAK ada fokus besar pada masalah keamanan, seperti serangan XSS. Mungkin seseorang membutuhkannya.

// Get base url
function getBaseUrl($array=false) {
    $protocol = "";
    $host = "";
    $port = "";
    $dir = "";  

    // Get protocol
    if(array_key_exists("HTTPS", $_SERVER) && $_SERVER["HTTPS"] != "") {
        if($_SERVER["HTTPS"] == "on") { $protocol = "https"; }
        else { $protocol = "http"; }
    } elseif(array_key_exists("REQUEST_SCHEME", $_SERVER) && $_SERVER["REQUEST_SCHEME"] != "") { $protocol = $_SERVER["REQUEST_SCHEME"]; }

    // Get host
    if(array_key_exists("HTTP_X_FORWARDED_HOST", $_SERVER) && $_SERVER["HTTP_X_FORWARDED_HOST"] != "") { $host = trim(end(explode(',', $_SERVER["HTTP_X_FORWARDED_HOST"]))); }
    elseif(array_key_exists("SERVER_NAME", $_SERVER) && $_SERVER["SERVER_NAME"] != "") { $host = $_SERVER["SERVER_NAME"]; }
    elseif(array_key_exists("HTTP_HOST", $_SERVER) && $_SERVER["HTTP_HOST"] != "") { $host = $_SERVER["HTTP_HOST"]; }
    elseif(array_key_exists("SERVER_ADDR", $_SERVER) && $_SERVER["SERVER_ADDR"] != "") { $host = $_SERVER["SERVER_ADDR"]; }
    //elseif(array_key_exists("SSL_TLS_SNI", $_SERVER) && $_SERVER["SSL_TLS_SNI"] != "") { $host = $_SERVER["SSL_TLS_SNI"]; }

    // Get port
    if(array_key_exists("SERVER_PORT", $_SERVER) && $_SERVER["SERVER_PORT"] != "") { $port = $_SERVER["SERVER_PORT"]; }
    elseif(stripos($host, ":") !== false) { $port = substr($host, (stripos($host, ":")+1)); }
    // Remove port from host
    $host = preg_replace("/:\d+$/", "", $host);

    // Get dir
    if(array_key_exists("SCRIPT_NAME", $_SERVER) && $_SERVER["SCRIPT_NAME"] != "") { $dir = $_SERVER["SCRIPT_NAME"]; }
    elseif(array_key_exists("PHP_SELF", $_SERVER) && $_SERVER["PHP_SELF"] != "") { $dir = $_SERVER["PHP_SELF"]; }
    elseif(array_key_exists("REQUEST_URI", $_SERVER) && $_SERVER["REQUEST_URI"] != "") { $dir = $_SERVER["REQUEST_URI"]; }
    // Shorten to main dir
    if(stripos($dir, "/") !== false) { $dir = substr($dir, 0, (strripos($dir, "/")+1)); }

    // Create return value
    if(!$array) {
        if($port == "80" || $port == "443" || $port == "") { $port = ""; }
        else { $port = ":".$port; } 
        return htmlspecialchars($protocol."://".$host.$port.$dir, ENT_QUOTES); 
    } else { return ["protocol" => $protocol, "host" => $host, "port" => $port, "dir" => $dir]; }
}
Mike
sumber