Pada chapter Clickjacking kita sudah mengirim dua header proteksi; sebenarnya keduanya hanyalah bagian kecil dari satu set instruksi keamanan yang bisa kita berikan kepada browser. Security headers adalah baris-baris kecil pada respons HTTP yang memberi tahu browser cara memperlakukan halaman Anda — jangan di-frame, jangan menebak tipe berkas, jangan kirim URL ke mana-mana. Biayanya nyaris nol karena tidak memerlukan logika aplikasi tambahan, tetapi setiap header menutup jalur serangan tertentu yang sudah kita bahas sepanjang section ini. Pada chapter ini kita susun seluruh set keamanan inti menjadi satu fungsi pembangkit yang bisa dipanggil dari setiap request.
Set Header Keamanan Inti #
Sebelum menulis kode, mari kita petakan dulu header apa saja yang layak dikirim dan serangan apa yang masing-masing ditangani. Tabel berikut merangkum enam header dengan nilai yang paling umum direkomendasikan:
| Header | Nilai yang direkomendasikan | Ancaman yang ditangani |
|---|---|---|
X-Content-Type-Options | nosniff | MIME sniffing yang mengubah berkas “polos” menjadi skrip aktif |
X-Frame-Options | DENY atau SAMEORIGIN | Clickjacking via iframe |
Strict-Transport-Security | max-age=31536000; includeSubDomains | Pencurian cookie lewat koneksi HTTP yang diturunkan |
Referrer-Policy | strict-origin-when-cross-origin | Kebocoran URL — termasuk token di query string — ke situs lain |
Permissions-Policy | camera=(), microphone=(), geolocation=() | Penyalahgunaan API perangkat lewat embed pihak ketiga |
Cache-Control | no-store (halaman sensitif) | Data privat tersimpan di cache bersama atau proxy |
Mari kita bahas peran masing-masing. X-Content-Type-Options: nosniff melarang browser menebak tipe konten berdasarkan isinya — tanpa header ini, berkas yang Anda klaim sebagai teks bisa dieksekusi sebagai skrip bila sniffing mendeteksi pola HTML, memperluas dampak serangan XSS
. X-Frame-Options menutup serangan clickjacking
persis seperti chapter sebelumnya. Strict-Transport-Security (HSTS) memerintahkan browser untuk selalu memakai HTTPS pada kunjungan berikutnya, memblokir skenario penyerang yang memaksa pengguna turun ke HTTP lalu mencuri cookie sesi
di jalur yang tidak terenkripsi.
Tiga header sisanya menjaga kebocoran data. Referrer-Policy membatasi informasi URL apa yang boleh dibawa header Referer saat pengguna mengeklik tautan keluar — nilai strict-origin-when-cross-origin hanya mengirim origin untuk situs pihak ketiga, sehingga query string seperti ?token=... tidak ikut bocor. Permissions-Policy mematikan API perangkat seperti kamera dan lokasi secara default, sehingga skrip pihak ketiga yang menyusup tidak bisa mengaktifkannya. Terakhir, Cache-Control: no-store pada halaman sensitif mencegah dashboard pengguna atau respons API privat tertinggal di cache komputer kantor atau proxy bersama.
Membangkitkan Set dari Satu Fungsi #
Prinsip pentingnya: seluruh set ini harus dikirim konsisten di setiap respons, bukan hanya halaman utama. Agar mudah diuji tanpa server, kita kemas sebagai fungsi murni yang mengembalikan array header:
<?php
function securityHeaders(array $overrides = []): array
{
$defaults = [
'X-Content-Type-Options' => 'nosniff',
'X-Frame-Options' => 'DENY',
'Referrer-Policy' => 'strict-origin-when-cross-origin',
'Permissions-Policy' => 'camera=(), microphone=(), geolocation=()',
'Strict-Transport-Security' => 'max-age=31536000; includeSubDomains',
'Cache-Control' => 'no-store',
];
return array_merge($defaults, $overrides);
}
foreach (securityHeaders() as $name => $value) {
echo "$name: $value\n";
}Output yang Dihasilkan:
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Strict-Transport-Security: max-age=31536000; includeSubDomains
Cache-Control: no-storeMari kita bedah fungsi di atas:
- Array
$defaultsmenjadi satu-satunya sumber kebenaran untuk nilai default; mengubah kebijakan cukup dilakukan di satu tempat. array_merge($defaults, $overrides)memungkinkan route tertentu menimpa satu header tanpa menulis ulang set — misalnya halaman statis yang ingin mengizinkan cache.- Fungsi tidak memanggil
header()secara langsung — pemisahan ini membuat fungsi dapat dijalankan dan diverifikasi di CLI, lalu dipakai oleh kode pengirim di lapisan web.
Menyesuaikan Konteks HTTPS dan Halaman Sensitif #
Dua header tadi perlu penyesuaian konteks. HSTS hanya bermakna bila situs sepenuhnya berjalan di atas HTTPS, sedangkan Cache-Control: no-store justru merugikan untuk konten publik yang justru ingin dimanfaatkan browser untuk kecepatan. Fungsi berikut menyesuaikan keduanya berdasarkan konteks request:
<?php
function buildHeaderSet(bool $isHttps, bool $isSensitive = true): array
{
$headers = [
'X-Content-Type-Options' => 'nosniff',
'X-Frame-Options' => 'DENY',
'Referrer-Policy' => 'strict-origin-when-cross-origin',
'Permissions-Policy' => 'camera=(), microphone=(), geolocation=()',
];
if ($isHttps) {
$headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains';
}
$headers['Cache-Control'] = $isSensitive ? 'no-store' : 'public, max-age=3600';
return $headers;
}
echo "Skenario HTTPS + halaman dashboard (sensitif):\n";
foreach (buildHeaderSet(true) as $name => $value) {
echo " $name: $value\n";
}
echo "Skenario HTTP + halaman artikel (publik):\n";
foreach (buildHeaderSet(false, false) as $name => $value) {
echo " $name: $value\n";
}Output yang Dihasilkan:
Skenario HTTPS + halaman dashboard (sensitif):
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Strict-Transport-Security: max-age=31536000; includeSubDomains
Cache-Control: no-store
Skenario HTTP + halaman artikel (publik):
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Cache-Control: public, max-age=3600Perhatikan dua keputusan pada fungsi tersebut. Pertama, Strict-Transport-Security hanya dimasukkan ketika $isHttps bernilai true — browser memang mengabaikan HSTS yang dikirim lewat HTTP, tetapi menyertakannya tetap menandakan konfigurasi yang belum dikelola dengan benar. Kedua, Cache-Control bercabang: no-store untuk data privat seperti dashboard dan respons JSON bersifat pribadi, sedangkan public, max-age=3600 untuk halaman artikel yang aman di-cache selama satu jam.
Menerapkan di Front Controller #
Di aplikasi nyata, pemanggilan fungsi ini ditempatkan di front controller — satu berkas PHP yang menangani semua request — atau pada middleware bila Anda memakai framework. Pola lengkapnya terlihat seperti berikut (memanggil header(), sehingga butuh server nyata untuk melihat hasilnya):
<?php
// keamanan-router.php — gabungkan dengan fungsi buildHeaderSet()
// di atas dalam satu berkas, lalu jalankan dengan php -S.
$isHttps = ($_SERVER['HTTPS'] ?? '') !== '';
foreach (buildHeaderSet($isHttps) as $name => $value) {
header("$name: $value");
}
header('Content-Type: text/html; charset=utf-8');
echo "<p>Dashboard pengguna.</p>";Perintah berikut menjalankan server pengembangan lalu memeriksa respons:
php -S 127.0.0.1:8099 keamanan-router.php
curl -I http://127.0.0.1:8099/Transcript nyata dari sesi pengujian tersebut memperlihatkan seluruh set terkirim dalam satu respons:
HTTP/1.1 200 OK
Host: 127.0.0.1:8099
Date: Tue, 13 Oct 2026 16:07:31 GMT
Connection: close
X-Powered-By: PHP/8.4.23
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Strict-Transport-Security: max-age=31536000; includeSubDomains
Cache-Control: no-store
Content-Type: text/html; charset=utf-8Transcript di atas diambil dari server pengembangan HTTP, jadi baris HSTS muncul hanya karena router memang memintanya; di produksi, pastikan server benar-benar berjalan di belakang TLS sebelum header tersebut aktif. Poin pentingnya adalah letak pemanggilan: header harus dipancarkan sebelum output apa pun, dan berlaku juga untuk respons error 404 atau 500 — penyerang justru senang mencari halaman yang lupa dilindungi.
Memeriksa Hasil dengan Pemindai #
Setelah set diterapkan, verifikasi menjadi rutinitas. Cara manual tetap sama seperti chapter sebelumnya: curl -I atau panel Network di DevTools. Untuk pemeriksaan menyeluruh, pemindai daring securityheaders.com memberi nilai huruf (A+ sampai F) terhadap situs Anda dan menunjukkan header mana yang belum ada — praktik yang bagus dilakukan setiap kali merilis perubahan konfigurasi server.
Ringkasan #
Security headers memberi pertahanan murah yang dibaca browser sebelum halaman dirender: nosniff menutup MIME sniffing, X-Frame-Options menutup clickjacking, HSTS mengunci HTTPS, Referrer-Policy menjaga URL, Permissions-Policy mematikan API perangkat, dan Cache-Control melindungi data sensitif dari cache. Seluruh set dibangkitkan dari satu fungsi murni lalu dipancarkan di front controller agar berlaku untuk semua respons. Set header ini bekerja karena browser membatasi dirinya — bagaimana bila kita justru perlu membuka akses lintas domain yang sah? Pertanyaan itu dijawab oleh mekanisme CORS pada chapter berikutnya.
Referensi #
- php.net — header()
- MDN — Strict-Transport-Security
- MDN — X-Content-Type-Options
- OWASP Secure Headers Project
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
contoh.php
Unduh berkas<?php
/* contoh.php - pembangkit set header keamanan siap pakai.
* Jalankan dengan: php contoh.php
*/
function buildHeaderSet(bool $isHttps, bool $isSensitive = true): array
{
$headers = [
'X-Content-Type-Options' => 'nosniff',
'X-Frame-Options' => 'DENY',
'Referrer-Policy' => 'strict-origin-when-cross-origin',
'Permissions-Policy' => 'camera=(), microphone=(), geolocation=()',
];
if ($isHttps) {
$headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains';
}
$headers['Cache-Control'] = $isSensitive ? 'no-store' : 'public, max-age=3600';
return $headers;
}
echo "Skenario HTTPS + halaman dashboard (sensitif):\n";
foreach (buildHeaderSet(true) as $name => $value) {
echo " $name: $value\n";
}
echo "Skenario HTTP + halaman artikel (publik):\n";
foreach (buildHeaderSet(false, false) as $name => $value) {
echo " $name: $value\n";
}