CSP (Content Security Policy) adalah header berisi daftar sumber yang boleh dipakai halaman: script dari mana, gambar dari mana, boleh menghubungi server mana. Browser menolak semua yang gak ada di daftar, termasuk script yang disisipkan penyerang lewat XSS. Bayangin daftar tamu di pintu gedung pernikahan. Satpam cuma memasukkan nama yang ada di daftar, dan tamu tak diundang tetap ditolak, serapi apa pun bajunya. Daftar tamunya CSP, satpamnya browser.
🧪 Tebakan cepat: policy halamanmu cuma
default-src 'self'. Halaman itu memuat<img src="https://images.com/banner.jpg">. Gambarnya tampil atau gak? Jawabannya ada di bagian "Directive dan fallback ke default-src".
Bentuk sebuah policy
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com
- Policy terdiri dari beberapa directive, dipisah titik koma dan spasi (
;). - Tiap directive = nama directive, spasi, lalu daftar sumber yang dipisah spasi.
script-src 'self' https://cdn.example.comartinya script boleh dari dua sumber. - Gak ada titik koma di ujung policy.
Sumber yang paling sering dipakai:
| Sumber | Artinya |
|---|---|
'self' |
hanya dari origin halaman sendiri (scheme, host, dan port sama). Subdomain gak termasuk |
https://cdn.example.com |
dari host itu saja |
data: |
URL data:, misalnya gambar kecil yang ditulis langsung di HTML |
'none' |
gak boleh dari mana pun |
'nonce-r4nd0m' |
inline script yang punya atribut nonce="r4nd0m" |
Kata kunci seperti 'self' dan 'none' wajib pakai kutip tunggal. Host dan data: ditulis tanpa kutip.
Directive dan fallback ke default-src
Tiap jenis sumber punya directive sendiri:
| Directive | Mengatur |
|---|---|
default-src |
cadangan untuk directive -src yang gak ditulis |
script-src |
JavaScript |
style-src |
CSS |
img-src |
gambar |
connect-src |
fetch, XHR, dan WebSocket |
font-src |
font |
frame-ancestors |
siapa yang boleh menampilkan halaman ini di iframe |
Directive yang gak ditulis jatuh ke default-src (kecuali frame-ancestors, base-uri, dan form-action, yang gak punya cadangan). Ini hasilnya untuk halaman https://myapp.com dengan policy default-src 'self' saja:
| Di halaman | Diatur oleh | Hasil |
|---|---|---|
<script src="/app.js"> |
default-src 'self' |
dimuat |
<script src="https://cdn.example.com/chart.js"> |
default-src 'self' |
diblokir |
<img src="https://images.com/banner.jpg"> |
default-src 'self' |
diblokir |
<img src="data:image/gif;base64,..."> |
default-src 'self' |
diblokir, 'self' gak mencakup data: |
fetch("https://api.myapp.com/orders") |
default-src 'self' |
diblokir, beda host |
Jadi gambar banner.jpg gak tampil. img-src gak ditulis, jadi yang dipakai default-src 'self', dan https://images.com bukan origin sendiri. Itu jawaban tebakannya. Chrome menulis di console:
Refused to load the image 'https://images.com/banner.jpg' because it violates the following Content Security Policy directive: "default-src 'self'". Note that 'img-src' was not explicitly set, so 'default-src' is used as a fallback.
Supaya semuanya dimuat, tulis directive-nya satu per satu:
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; img-src 'self' data: https://images.com; connect-src 'self' https://api.myapp.com
Inline script: nonce, bukan unsafe-inline
Begitu ada script-src (atau default-src) tanpa 'unsafe-inline', browser menolak semua script yang ditulis langsung di HTML: <script>...</script> maupun onclick="...". Itu yang menghentikan XSS. Komentar berisi <script>alert(1)</script> gak jalan, dan Chrome menulis Refused to execute inline script because it violates the following Content Security Policy directive: ....
Lalu gimana dengan inline script milikmu sendiri? Pakai nonce: angka acak yang dibuat server baru untuk setiap response, ditulis di header dan di tag script-mu:
<!-- Content-Security-Policy: script-src 'self' 'nonce-r4nd0m' -->
<script nonce="r4nd0m">initCart()</script> <!-- jalan -->
<script>alert(1)</script> <!-- diblokir: gak punya nonce -->
Penyerang gak tahu nonce response itu, jadi script sisipannya gak bisa ikut. Di production, nonce harus acak dan berganti tiap response, bukan r4nd0m yang tetap seperti di contoh ini.
Dua kata kunci yang membatalkan perlindungan ini:
'unsafe-inline'mengizinkan semua inline script, termasuk sisipan penyerang.'unsafe-eval'mengizinkaneval()dan sejenisnya, yang menjalankan teks sebagai kode.
Jadi script-src 'unsafe-inline' 'unsafe-eval' mengizinkan inline script dan eval, dan XSS jalan lagi seperti tanpa CSP. Satu catatan: kalau daftar sumber sudah berisi nonce, browser mengabaikan 'unsafe-inline'.
Membangun string policy dengan JavaScript
Di server, policy sering disusun dari object, lalu dijadikan string. Ini function latihan:
function buildCSP(directives) {
return Object.entries(directives)
.map(([name, sources]) => name + " " + sources.join(" "))
.join("; ");
}
console.log(buildCSP({
"default-src": ["'self'"],
"script-src": ["'self'", "https://cdn.example.com"]
}));
// default-src 'self'; script-src 'self' https://cdn.example.com
| Langkah | Hasil |
|---|---|
Object.entries(directives) |
[["default-src", ["'self'"]], ["script-src", ["'self'", "https://cdn.example.com"]]] |
.map(...) |
["default-src 'self'", "script-src 'self' https://cdn.example.com"] |
.join("; ") |
"default-src 'self'; script-src 'self' https://cdn.example.com" |
Object.entriesmengubah object jadi daftar pasangan[nama, sumber], urut sesuai urutan kunci di object.sources.join(" ")menyambung sumber dengan spasi, laluname + " " + ...menaruh nama directive di depannya..join("; ")menyambung antar directive. Penyambung cuma dipasang di antara potongan, jadi object dengan satu directive menghasilkan"default-src 'self'"tanpa titik koma.
Header, meta tag, dan Report-Only
CSP paling baik dikirim sebagai header. Kalau gak bisa mengatur header, pakai meta tag di <head>:
<meta http-equiv="Content-Security-Policy" content="default-src 'self'">
Meta tag cuma berlaku untuk isi halaman setelah tag itu, dan gak bisa memakai frame-ancestors, report-uri, atau mode Report-Only.
Sebelum memblokir di production, coba dulu dengan header Report-Only. Browser tetap memuat semuanya, tapi mencatat pelanggaran di console (diawali [Report Only]) dan mengirim laporannya ke URL di report-uri:
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report
Setelah laporannya bersih beberapa hari, ganti nama header-nya jadi Content-Security-Policy.
⚠️ Jebakan yang sering ketemu
'unsafe-inline'atau'unsafe-eval'discript-src. Dua kata kunci itu membuka lagi pintu yang mau ditutup CSP. Pakai nonce untuk inline script milikmu.- Lupa kutip.
script-src selfdibaca sebagai host bernamaself, bukan origin-mu, jadi/app.jsikut diblokir. Tulis'self'. - Mengira
default-srcmenjaga semuanya.frame-ancestors,base-uri, danform-actiongak jatuh kedefault-src. Tulis sendiri kalau dibutuhkan. - Format string yang salah. Di
buildCSP, antar directive pakai"; "dan antar sumber pakai" ". Jangan ada titik koma di ujung.
TL;DR: CSP (Content Security Policy) adalah daftar sumber yang boleh dipakai halaman, dan browser menolak sisanya, termasuk script sisipan XSS. Policy = directive dipisah ; , tiap directive = nama + sumber dipisah spasi, seperti default-src 'self'; script-src 'self' https://cdn.example.com. 'self' artinya hanya dari origin sendiri. img-src mengatur gambar, script-src script, connect-src fetch, dan yang gak ditulis jatuh ke default-src. Inline script diblokir kecuali punya nonce; 'unsafe-inline' dan 'unsafe-eval' membatalkan perlindungannya. Pasang lewat header, atau <meta http-equiv="Content-Security-Policy" content="...">, dan uji dulu dengan Content-Security-Policy-Report-Only.