Menerapkan CSP Nonce dan Hash: Mengamankan Skrip Dinamis di Aplikasi Web Modern
1. Pendahuluan
Di dunia pengembangan web yang serba cepat, keamanan adalah prioritas utama. Salah satu pertahanan terkuat yang bisa kita terapkan di sisi klien adalah Content Security Policy (CSP). CSP berfungsi sebagai daftar putih (whitelist) sumber daya yang diizinkan untuk dimuat oleh browser, seperti skrip, gaya, gambar, dan lainnya. Tujuannya? Mencegah serangan Cross-Site Scripting (XSS), data injection, dan berbagai kerentanan lain yang memanfaatkan skrip atau konten berbahaya.
Namun, seringkali developer dihadapkan pada dilema: bagaimana mengimplementasikan CSP yang ketat tanpa mengorbankan fungsionalitas aplikasi yang membutuhkan skrip atau gaya inline? Banyak yang akhirnya terpaksa menggunakan unsafe-inline dalam kebijakan CSP mereka, yang secara efektif membuka kembali pintu bagi banyak serangan XSS yang ingin dicegah oleh CSP itu sendiri. 🤦♂️
Di artikel ini, kita akan menyelami dua jurus rahasia CSP yang memungkinkan Anda mengamankan skrip dan gaya inline tanpa unsafe-inline: nonce (Number once) dan hash. Kami akan membahas kapan menggunakan masing-masing, bagaimana mengimplementasikannya secara praktis di aplikasi web modern, dan tips-tips untuk menjaga keamanan Anda tetap optimal.
📌 Mengapa Ini Penting?
Menggunakan unsafe-inline sama dengan memasang kunci ganda di pintu depan, tapi meninggalkan jendela belakang terbuka lebar. nonce dan hash adalah cara kita untuk menutup jendela itu dengan cerdas.
2. Memahami Nonce dan Hash dalam CSP
Baik nonce maupun hash adalah mekanisme yang memungkinkan browser memverifikasi bahwa skrip atau gaya inline tertentu berasal dari sumber yang tepercaya, bukan injeksi dari penyerang.
2.1. Nonce (Number Once)
Nonce adalah string acak kriptografis yang dihasilkan unik untuk setiap permintaan HTTP. String ini kemudian disertakan dalam header Content-Security-Policy dan juga sebagai atribut nonce pada setiap elemen <script> atau <style> inline yang ingin diizinkan.
Bagaimana cara kerjanya?
- Saat server menerima permintaan halaman, ia menghasilkan sebuah
noncebaru yang unik. - Server menyertakan
nonceini dalam headerContent-Security-Policy(misalnya,script-src 'nonce-RANDOMSTRING'). - Server juga menyuntikkan
nonceyang sama ke dalam atributnoncepada setiap tag<script>atau<style>inline di halaman HTML yang ingin diizinkan (misalnya,<script nonce="RANDOMSTRING">alert('Hello');</script>). - Browser akan mengeksekusi skrip atau gaya inline hanya jika atribut
nonce-nya cocok dengannoncedi header CSP.
Kapan menggunakan Nonce?
- Skrip Dinamis: Sempurna untuk skrip atau gaya yang dihasilkan secara dinamis oleh server, di mana kontennya mungkin berubah atau tidak dapat di-hash sebelumnya.
- Server-Side Rendering (SSR): Nonce sangat cocok untuk aplikasi yang dirender di sisi server, karena server memiliki kontrol penuh untuk menyuntikkan nonce baik ke header CSP maupun ke tag skrip/gaya.
Kelebihan:
- Fleksibel: Dapat digunakan untuk skrip inline yang kontennya dinamis.
- Keamanan Per-Permintaan: Karena nonce unik per permintaan, penyerang tidak dapat dengan mudah memprediksi atau menggunakan kembali nonce untuk menyuntikkan skrip mereka.
Kekurangan:
- Membutuhkan Backend: Anda harus memiliki server yang menghasilkan nonce untuk setiap permintaan dan menyuntikkannya ke HTML. Ini tidak cocok untuk situs statis murni.
- Kompleksitas: Integrasi ke dalam pipeline rendering server bisa sedikit rumit, terutama jika banyak skrip inline.
2.2. Hash
Hash adalah representasi kriptografis dari konten skrip atau gaya inline tertentu. Anda menghitung hash dari isi persis dari skrip atau gaya inline, lalu menyertakan hash tersebut dalam header Content-Security-Policy.
Bagaimana cara kerjanya?
- Anda menghitung hash (misalnya, SHA256, SHA384, atau SHA512) dari konten skrip atau gaya inline Anda.
- Anda menyertakan hash ini dalam header
Content-Security-Policy(misalnya,script-src 'sha256-BASE64HASH'). - Browser akan mengeksekusi skrip atau gaya inline hanya jika hash kontennya cocok persis dengan salah satu hash yang terdaftar di header CSP.
Kapan menggunakan Hash?
- Skrip/Gaya Statis: Ideal untuk skrip atau gaya inline yang kontennya tetap dan tidak berubah. Contohnya adalah skrip kecil untuk inisialisasi atau gaya penting (critical CSS).
- Situs Statis: Bisa digunakan untuk situs statis murni, karena hash dapat dihitung saat build time dan ditanamkan ke header CSP.
Kelebihan:
- Tidak Membutuhkan Backend Dinamis: Hash dapat dihitung saat waktu build atau secara manual.
- Sederhana untuk Konten Statis: Cukup hitung hash-nya sekali dan tambahkan ke CSP.
Kekurangan:
- Rapuh terhadap Perubahan: Bahkan perubahan sekecil apa pun (spasi, komentar) pada skrip atau gaya inline akan mengubah hash-nya, sehingga CSP akan memblokirnya. Ini bisa menjadi tantangan dalam proses pengembangan.
- Tidak Cocok untuk Konten Dinamis: Jika konten skrip atau gaya inline sering berubah, menghitung dan memperbarui hash secara terus-menerus menjadi tidak praktis.
🎯 Pilihan Terbaik:
Gunakan nonce untuk skrip inline yang dinamis dan dirender server. Gunakan hash untuk skrip atau gaya inline yang statis dan jarang berubah.
3. Implementasi Nonce di Aplikasi SSR/Server-Rendered
Mari kita lihat contoh praktis implementasi nonce menggunakan Node.js dengan Express dan template engine seperti EJS.
// app.js (Contoh Backend Node.js dengan Express)
const express = require('express');
const crypto = require('crypto'); // Untuk menghasilkan nonce
const app = express();
const port = 3000;
// Set EJS sebagai template engine
app.set('view engine', 'ejs');
app.set('views', './views');
// Middleware untuk menghasilkan nonce dan menyertakannya di res.locals
app.use((req, res, next) => {
res.locals.nonce = crypto.randomBytes(16).toString('base64');
next();
});
// Route utama
app.get('/', (req, res) => {
const nonce = res.locals.nonce;
// Set Content-Security-Policy header
// Penting: 'self' mengizinkan skrip dari domain yang sama
// 'nonce-' directive mengizinkan skrip dengan atribut nonce yang cocok
res.setHeader(
'Content-Security-Policy',
`default-src 'self'; ` +
`script-src 'self' 'nonce-${nonce}'; ` + // Izinkan skrip dari 'self' dan yang memiliki nonce
`style-src 'self' 'nonce-${nonce}'; ` + // Izinkan gaya dari 'self' dan yang memiliki nonce
`img-src 'self' data:; ` +
`connect-src 'self';`
);
res.render('index', { nonce: nonce });
});
app.listen(port, () => {
console.log(`Server berjalan di http://localhost:${port}`);
});
<!-- views/index.ejs (Contoh Template EJS) -->
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Aplikasi dengan CSP Nonce</title>
<!-- Gaya inline yang diizinkan dengan nonce -->
<style nonce="<%= nonce %>">
body {
font-family: sans-serif;
margin: 20px;
background-color: #f4f4f4;
color: #333;
}
.highlight {
color: #007bff;
font-weight: bold;
}
</style>
</head>
<body>
<h1>Selamat Datang di Aplikasi Aman!</h1>
<p>Ini adalah halaman yang dilindungi dengan <span class="highlight">Content Security Policy</span>.</p>
<button id="myButton">Klik Saya</button>
<!-- Skrip inline yang diizinkan dengan nonce -->
<script nonce="<%= nonce %>">
// Skrip ini akan dieksekusi karena memiliki nonce yang cocok
document.getElementById('myButton').addEventListener('click', () => {
alert('Tombol diklik dengan skrip aman!');
});
</script>
<!-- Skrip inline ini TIDAK akan dieksekusi karena tidak memiliki nonce -->
<!-- <script>
alert('Skrip berbahaya!');
</script> -->
<!-- Skrip eksternal (jika diizinkan oleh 'self' atau domain lain) -->
<script src="/path/to/my-script.js" nonce="<%= nonce %>"></script>
<!-- Catatan: Untuk skrip eksternal, nonce tidak terlalu dibutuhkan jika domain sudah diizinkan oleh 'self'.
Namun, menyertakannya juga tidak ada salahnya dan bisa menjadi pertahanan tambahan.
Beberapa browser mungkin mengabaikan nonce untuk skrip eksternal jika domain sudah diizinkan. -->
</body>
</html>
Dalam contoh di atas, setiap kali halaman index diakses, server akan menghasilkan nonce baru. nonce ini kemudian digunakan untuk atribut nonce pada tag <style> dan <script> inline, serta dalam header CSP. Browser akan memverifikasi nonce ini sebelum mengeksekusi konten inline.
⚠️ Penting: Pastikan nonce Anda benar-benar acak dan unik per respons. Jika nonce dapat diprediksi atau digunakan kembali, penyerang dapat menyuntikkan skrip mereka.
4. Implementasi Hash di Aplikasi Statis/Build Time
Untuk aplikasi statis atau skrip/gaya inline yang kontennya tidak berubah, hash adalah pilihan yang lebih baik. Proses ini biasanya terintegrasi dengan build process Anda.
Contoh Sederhana (Manual/Build Script):
Misalkan Anda memiliki skrip inline berikut:
<!-- index.html -->
<script>
console.log('Ini adalah skrip inisialisasi.');
</script>
Untuk mendapatkan hash-nya:
-
Salin persis konten skrip (termasuk spasi, karakter baris baru, dll.):
console.log('Ini adalah skrip inisialisasi.'); -
Gunakan tool untuk menghitung hash SHA256 (bisa online, atau via Node.js):
// hash-generator.js const crypto = require('crypto'); const scriptContent = `console.log('Ini adalah skrip inisialisasi.');`; // Perhatikan new line jika ada const hash = crypto.createHash('sha256').update(scriptContent).digest('base64'); console.log(`sha256-${hash}`); // Output: sha256-R1eI9n3m2oK0p7q8s5t4u6v7w8x9y0z1A2B3C4D5E6F7G8H9I0J1K2L3M4N5O6P7Q= (contoh) -
Tambahkan hash ini ke header CSP Anda:
Content-Security-Policy: script-src 'self' 'sha256-R1eI9n3m2oK0p7q8s5t4u6v7w8x9y0z1A2B3C4D5E6F7G8H9I0J1K2L3M4N5O6P7Q=';
Integrasi dengan Build Tools (Webpack/Vite)
Untuk proyek yang lebih besar, Anda tidak akan menghitung hash secara manual. Anda akan menggunakan plugin build tool.
- Webpack: Ada plugin seperti
webpack-subresource-integrity(meskipun lebih untuk SRI, konsep hashingnya mirip) atau plugin kustom yang dapat membaca HTML, menghitung hash skrip/gaya inline, dan menyuntikkannya ke file atau variabel yang kemudian digunakan untuk membuat header CSP. - Vite: Karena Vite menggunakan Rollup, Anda bisa mencari plugin Rollup yang sesuai atau membuat script
post-buildyang memindai output HTML Anda untuk skrip/gaya inline dan menghasilkan daftar hash.
💡 Tips: Untuk skrip inline yang sangat kecil atau critical CSS yang harus dimuat secepat mungkin, hash adalah pilihan yang bagus. Pastikan untuk mengotomatiskan perhitungannya dalam pipeline CI/CD Anda agar tidak ada kesalahan saat konten berubah.
5. Menangani Skrip Pihak Ketiga dan Event Handler Inline
Penggunaan nonce dan hash akan sangat membantu, tetapi ada beberapa skenario yang memerlukan perhatian khusus:
5.1. Skrip Pihak Ketiga
Skrip pihak ketiga (misalnya, Google Analytics, Stripe.js, widget chat) seringkali sulit diatur dengan nonce atau hash jika mereka menyuntikkan skrip inline mereka sendiri atau menggunakan eval().
❌ Masalah: Anda tidak memiliki kontrol atas kode mereka untuk menambahkan atribut nonce atau menghitung hash dari setiap skrip inline yang