WEB-SECURITY SECURITY-HEADERS CSP FRONTEND-SECURITY APPLICATION-SECURITY DEVSECOPS BEST-PRACTICES JAVASCRIPT BUILD-TOOLS

Menerapkan CSP Nonce dan Hash: Mengamankan Skrip Dinamis di Aplikasi Web Modern

⏱️ 9 menit baca
👨‍💻

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?

  1. Saat server menerima permintaan halaman, ia menghasilkan sebuah nonce baru yang unik.
  2. Server menyertakan nonce ini dalam header Content-Security-Policy (misalnya, script-src 'nonce-RANDOMSTRING').
  3. Server juga menyuntikkan nonce yang sama ke dalam atribut nonce pada setiap tag <script> atau <style> inline di halaman HTML yang ingin diizinkan (misalnya, <script nonce="RANDOMSTRING">alert('Hello');</script>).
  4. Browser akan mengeksekusi skrip atau gaya inline hanya jika atribut nonce-nya cocok dengan nonce di header CSP.

Kapan menggunakan Nonce?

Kelebihan:

Kekurangan:

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?

  1. Anda menghitung hash (misalnya, SHA256, SHA384, atau SHA512) dari konten skrip atau gaya inline Anda.
  2. Anda menyertakan hash ini dalam header Content-Security-Policy (misalnya, script-src 'sha256-BASE64HASH').
  3. 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?

Kelebihan:

Kekurangan:

🎯 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:

  1. Salin persis konten skrip (termasuk spasi, karakter baris baru, dll.): console.log('Ini adalah skrip inisialisasi.');

  2. 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)
  3. 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.

💡 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