Pertahanan sejauh ini bertumpu pada disiplin kita sendiri: textContent alih-alih innerHTML, tidak ada eval, tautan tervalidasi, dependensi terjaga. Semua itu benar, tetapi semua itu bergantung pada manusia yang tidak pernah lupa. Content Security Policy (CSP) menambah lapisan yang tidak bergantung pada kehati-hatian: server mengirim header HTTP yang menyatakan dari mana halaman boleh memuat skrip, gaya, dan sumber daya lain — dan browser-lah yang menegakkannya, script demi script.
Lapisan ini bekerja sebagai jaring pengaman: bila satu celah terlewat — ulasan berbahaya yang lolos, dependensi yang disusupi — skrip yang tidak sesuai kebijakan tidak dijalankan browser. Chapter ini membahas bentuk kebijakannya, arahan yang paling sering dipakai, penanganan skrip inline dengan nonce, dan mode laporan untuk uji coba.
Bentuk Kebijakan #
CSP dikirim sebagai header HTTP dari server. Satu contoh kebijakan untuk halaman toko:
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'Isinya daftar arahan yang dipisah titik koma; setiap arahan diikuti daftar sumber yang diizinkan. Kata kunci ditulis dengan tanda kutip tunggal: 'self' berarti origin halaman itu sendiri, 'none' berarti tidak ada, 'unsafe-inline' mengizinkan kode inline. Arahan yang paling sering dipakai:
| Arahan | Mengatur |
|---|---|
default-src | bawaan bagi arahan yang tidak ditulis di kebijakan |
script-src | sumber skrip yang boleh dimuat dan dijalankan |
style-src | sumber stylesheet yang boleh dimuat |
img-src, connect-src | gambar, dan tujuan fetch/XHR |
Contoh di atas berarti: skrip hanya dari situs sendiri dan dari cdn.example.com; gaya dari situs sendiri plus inline; sisanya — gambar, font, tujuan fetch — mengikuti default-src 'self'.
Mekanisme fallback inilah yang membuat kebijakan pendek tetap aman: kita tidak perlu menuliskan semua arahan dari awal. Kebijakan satu baris seperti default-src 'self' sudah memblokir skrip inline, skrip dari CDN mana pun, dan semua sumber daya lintas origin — penerapan yang realistis biasanya menambahkan arahan khusus untuk kebutuhan yang memang ada, satu per satu, lewat mode laporan yang dibahas di bawah.
Membaca Kebijakan dengan Model Sederhana #
Untuk membiasakan cara baca kebijakan, kita susun model kecil yang mengurai teks header dan menjawab pertanyaan “apakah sumber ini diizinkan?”. Model ini untuk belajar — browser memakai aturan lengkap dengan nonce, hash, dan path — tetapi mekanisme daftar izin dan fallback-nya sama:
// baca-kebijakan.js — model sederhana untuk membaca isi kebijakan CSP
function uraiKebijakan(header) {
const arahan = new Map();
for (const bagian of header.split(";")) {
const potongan = bagian.trim().split(/\s+/);
const nama = potongan[0];
const sumber = potongan.slice(1);
if (nama !== "") arahan.set(nama, sumber);
}
return arahan;
}
const header =
"default-src 'self'; " +
"script-src 'self' https://cdn.example.com; " +
"style-src 'self' 'unsafe-inline'";
const kebijakan = uraiKebijakan(header);
for (const [nama, sumber] of kebijakan) {
console.log(nama, "=>", sumber.join(" "));
}
// bila arahan khusus tidak ada, browser jatuh ke default-src
function sumberDiizinkan(arahan, nama, sumberTunggal) {
const daftar = arahan.get(nama) ?? arahan.get("default-src") ?? [];
return daftar.includes(sumberTunggal);
}
console.log("script 'self' :", sumberDiizinkan(kebijakan, "script-src", "'self'"));
console.log("script cdn-lain :", sumberDiizinkan(kebijakan, "script-src", "https://cdn-lain.example"));
console.log("img 'self' (fallback) :", sumberDiizinkan(kebijakan, "img-src", "'self'"));
console.log("style cdn.example.com :", sumberDiizinkan(kebijakan, "style-src", "https://cdn.example.com"));Output yang Dihasilkan:
default-src => 'self'
script-src => 'self' https://cdn.example.com
style-src => 'self' 'unsafe-inline'
script 'self' : true
script cdn-lain : false
img 'self' (fallback) : true
style cdn.example.com : falseMari kita bedah:
- Baris 2–11 (uraiKebijakan): header dipisah di setiap
;, tiap bagian dipecah lagi berdasarkan spasi — kata pertama menjadi nama arahan, sisanya daftar sumber. HasilnyaMapdari nama arahan ke daftar sumber, persis struktur yang dibaca browser. - Baris 13–17 (kebijakan contoh): teks yang sama dengan contoh header di atas, disusun lewat penyambungan agar muat di layar.
- Baris 19–21 (pembacaan arahan): keluaran tiga baris pertama memperlihatkan isi
Map— tiga arahan, masing-masing dengan daftar sumbernya. - Baris 24–27 (aturan fallback):
arahan.get(nama) ?? arahan.get("default-src") ?? []meniru perilaku browser: arahan khusus yang tidak ditulis jatuh kedefault-src. Inilah sebabnyaimg-src— yang tidak pernah ditulis — tetap menjawabtrueuntuk'self'(keluaran ke-3 setelah loop). - Baris 29–32 (empat jawaban): skrip dari
'self'diizinkan;cdn-lain.exampleditolak meski mirip dengan CDN yang diizinkan — perbandingannya persis, bukan berawalan. Jawaban terakhir jugafalse: arahan saling berdiri sendiri, jadihttps://cdn.example.comyang diizinkan discript-srctidak otomatis diizinkan distyle-src.
Pola “daftar izin + perbandingan persis” inilah inti CSP — sama dengan pola daftar izin protokol di chapter URL Validation, kali ini ditegakkan browser.
Skrip Inline dan Nonce #
Kebijakan script-src 'self' memblokir semua skrip inline — termasuk yang ditulis aplikasi sendiri di dalam HTML. Peluruhan yang lazim adalah menambahkan 'unsafe-inline', yang mengembalikan seluruh kekuatan perlindungan. Pengganti yang tepat adalah nonce: nilai acak yang dibuat server untuk setiap permintaan halaman, dicantumkan di kebijakan dan di tag yang sah:
Content-Security-Policy: script-src 'nonce-d4f7K2xQ9p'
<!-- tag ini ditulis server; nilai nonce-nya sama dengan di header -->
<script nonce="d4f7K2xQ9p">mulaiHalaman();</script>Browser hanya menjalankan <script> yang atribut nonce-nya cocok dengan kebijakan. Skrip yang diselundupkan lewat innerHTML tidak membawa nonce yang sah, sehingga diblokir meski berhasil masuk ke DOM. Nilai nonce harus acak dan berbeda pada tiap permintaan — itu pekerjaan server, di luar cakupan JavaScript klien. Syarat keacakannya bukan pelengkap: bila nonce bisa ditebak atau dipakai ulang antar permintaan, penyerang cukup menyalin nilai yang sama, dan perlindungannya kembali ke nol.
Arahan style-src layak mendapat perlakuan yang sama seriusnya. Gaya inline yang menyusup tidak langsung menjalankan kode, tetapi cukup untuk mengubah tata letak, menampilkan konten palsu di atas halaman, atau menyelundupkan data lewat URL gambar pada properti CSS — kerusakannya berbeda bentuk, tetapi tetap kerusakan.
Yang Tidak Dilindungi CSP #
Supaya ekspektasinya pas: CSP membatasi sumber kode, bukan memperbaiki logika yang salah. Halaman yang menulis ulasan pengguna lewat innerHTML tetap menampilkan markup yang diinjeksi — CSP hanya mencegah skrip dari sumber yang tidak diizinkan ikut berjalan. Escape dengan textContent, validasi URL, dan sanitasi tetap menjadi garis pertama; CSP adalah jaring pengaman yang mengurangi akibat bila garis pertama bocor. Kebijakan juga tidak memengaruhi perilaku server sama sekali — data yang salah tetap salah, apa pun headernya.
Mode Report-Only #
Sebelum kebijakan ditegakkan, mode laporan memungkinkan kita mengamatinya tanpa risiko mematikan halaman sendiri: header yang dipakai bernama Content-Security-Policy-Report-Only, dengan isi kebijakan yang sama. Browser tidak memblokir apa pun — ia hanya mencatat setiap pelanggaran ke console dan mengirim laporannya ke endpoint yang ditunjuk arahan report-uri (atau report-to pada spesifikasi yang lebih baru). Dari laporan itulah kita melihat resource mana yang akan terdampak, lalu melengkapi kebijakan sebelum versi Content-Security-Policy sungguhan diterapkan.
Lapisan browser ini mengatur kode yang boleh berjalan. Chapter terakhir mengatur data yang boleh tertinggal di browser setelah kode selesai berjalan.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
baca-kebijakan.js
Unduh berkas// baca-kebijakan.js — model sederhana untuk membaca isi kebijakan CSP
function uraiKebijakan(header) {
const arahan = new Map();
for (const bagian of header.split(";")) {
const potongan = bagian.trim().split(/\s+/);
const nama = potongan[0];
const sumber = potongan.slice(1);
if (nama !== "") arahan.set(nama, sumber);
}
return arahan;
}
const header =
"default-src 'self'; " +
"script-src 'self' https://cdn.example.com; " +
"style-src 'self' 'unsafe-inline'";
const kebijakan = uraiKebijakan(header);
for (const [nama, sumber] of kebijakan) {
console.log(nama, "=>", sumber.join(" "));
}
// bila arahan khusus tidak ada, browser jatuh ke default-src
function sumberDiizinkan(arahan, nama, sumberTunggal) {
const daftar = arahan.get(nama) ?? arahan.get("default-src") ?? [];
return daftar.includes(sumberTunggal);
}
console.log("script 'self' :", sumberDiizinkan(kebijakan, "script-src", "'self'"));
console.log("script cdn-lain :", sumberDiizinkan(kebijakan, "script-src", "https://cdn-lain.example"));
console.log("img 'self' (fallback) :", sumberDiizinkan(kebijakan, "img-src", "'self'"));
console.log("style cdn.example.com :", sumberDiizinkan(kebijakan, "style-src", "https://cdn.example.com"));