Content Security Policy (CSP) — Security

Apa itu Content Security Policy? CSP adalah HTTP header yang memberi tahu browser sumber mana yang boleh memuat resource (script, style, image, dll.). CSP…

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

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:

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"

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

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.

Yang akan kamu pelajari