Serangan Cross-Site Scripting (XSS) secara konsisten menempati daftar ancaman keamanan aplikasi web paling berbahaya menurut OWASP Top 10. Meskipun pengembang modern telah memanfaatkan sanitasi input dan escaping otomatis pada library antarmuka seperti React, celah keamanan tetap dapat timbul dari injeksi dependensi npm pihak ketiga (supply chain attack), kerentanan pada parsing atribut HTML tak terduga, atau manipulasi skrip analitik eksternal.
Solusi pertahanan lapis kedua yang paling efektif untuk membatasi dampak eksekusi kode berbahaya di browser pengguna adalah penerapan header HTTP Content Security Policy (CSP) Level 3. Dengan CSP yang terkonfigurasi ketat, browser akan menolak mengeksekusi skrip berbahaya meskipun payload XSS berhasil menembus lapisan HTML aplikasi.
Alur Kerja Nonce-Based Content Security Policy Level 3:
1. Permintaan Masuk ────> [Next.js Middleware]
│ Menghasilkan kriptografis unik: nonce = base64(randomBytes)
▼
[HTTP Response Headers]
Content-Security-Policy:
script-src 'nonce-d83a1b...' 'strict-dynamic';
│
2. Browser Memvalidasi Skrip ▼
┌───────────────────────────────────────────────┐
│ <script nonce="d83a1b..."> // Diizinkan │
│ <script>alert(document.cookie)</script> │
│ └──> [DITOLAK BROWSER: Missing Valid Nonce] │
└───────────────────────────────────────────────┘
Mengapa Pendekatan Whitelist Domain CSP Level 2 Telah Usang? #
Pada standar CSP Level 1 dan Level 2 generasi lama, pengembang umumnya menulis aturan berbasis daftar domain yang diizinkan (allowlist), contohnya:
Content-Security-Policy: script-src 'self' https://apis.google.com https://cdnjs.cloudflare.com;
Pendekatan ini memiliki kelemahan mendasar yang fatal. Jika aplikasi mengizinkan CDN publik seperti cdnjs.cloudflare.com atau domain Google APIs, peretas dapat menyisipkan payload XSS yang memuat endpoint JSONP rentan atau library lama yang memiliki bug eksekusi kode dari CDN yang sama. Akibatnya, browser tetap mengeksekusi payload jahat karena domain asalnya cocok dengan whitelist.
CSP Level 3 mengatasi masalah ini dengan beralih dari daftar domain ke sistem kriptografi berbasis nonce (number used once) yang dipadukan dengan direktif 'strict-dynamic'.
Dengan 'strict-dynamic':
- Browser modern hanya akan mengeksekusi tag skrip yang memiliki atribut
noncepersis sama dengan nilai acak yang dikirimkan server pada header HTTP. - Skrip tepercaya yang memuat nonce tersebut berhak membuat tag skrip turunan secara dinamis tanpa perlu mendaftarkan setiap sub-domain skrip baru ke dalam whitelist header.
- Browser akan mengabaikan keyword
'unsafe-inline',http:, atau daftar domain lama jika'strict-dynamic'aktif, memastikan backward compatibility bagi browser generasi lama tanpa mengorbankan keamanan browser modern.
Implementasi Nonce Generator pada Next.js Middleware #
Dalam Next.js App Router, implementasi nonce harus dilakukan di dalam middleware agar setiap request HTTP yang masuk menerima nilai token kriptografis yang benar-benar acak dan segar.
Berikut adalah implementasi berkas middleware.ts pada root project Next.js:
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
export function middleware(request: NextRequest) {
// 1. Buat nonce kriptografis berbasis base64 (16 byte acak)
const nonce = Buffer.from(crypto.randomUUID()).toString('base64');
// 2. Susun direktif CSP Level 3 yang komprehensif
const cspHeader = `
default-src 'self';
script-src 'self' 'nonce-${nonce}' 'strict-dynamic' 'report-sample';
style-src 'self' 'unsafe-inline';
img-src 'self' blob: data: https://images.unsplash.com https://puluhlabs.com;
font-src 'self' data:;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
connect-src 'self' https://vitals.vercel-insights.com https://analytics.google.com;
upgrade-insecure-requests;
`.replace(/\s{2,}/g, ' ').trim();
// 3. Masukkan nonce ke header request internal agar dapat dibaca Server Components
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-nonce', nonce);
requestHeaders.set('content-security-policy', cspHeader);
// 4. Teruskan request dengan menyertakan CSP pada response headers
const response = NextResponse.next({
request: {
headers: requestHeaders,
},
});
response.headers.set('Content-Security-Policy', cspHeader);
return response;
}
export const config = {
matcher: [
{
source: '/((?!api|_next/static|_next/image|favicon.ico|robots.txt).*)',
missing: [
{ type: 'header', key: 'next-router-prefetch' },
{ type: 'header', key: 'purpose', value: 'prefetch' },
],
},
],
};
Mengonsumsi Nilai Nonce pada Root Layout Next.js #
Setelah middleware menyisipkan x-nonce ke dalam header request, komponen app/layout.tsx dapat membaca header tersebut untuk disematkan ke elemen skrip aplikasi dan pelacak eksternal seperti Google Tag Manager.
// app/layout.tsx
import { headers } from 'next/headers';
import Script from 'next/script';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const nonce = headers().get('x-nonce') || undefined;
return (
<html lang="id">
<head>
{/* Skrip Analitik Eksternal yang diizinkan melalui nonce */}
<Script
id="google-tag-manager"
strategy="afterInteractive"
nonce={nonce}
dangerouslySetInnerHTML={{
__html: `
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-XXXXXXX');
`,
}}
/>
</head>
<body>{children}</body>
</html>
);
}
Memantau Pelanggaran dengan Mode Report-Only #
Menerapkan CSP secara langsung pada aplikasi skala besar berisiko memecah fungsionalitas antarmuka apabila terdapat skrip pihak ketiga (seperti widget customer service atau payment gateway iframe) yang belum didaftarkan dengan benar.
Langkah mitigasi paling bijak adalah mengaktifkan mode pelaporan selama masa uji coba 14 hari:
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' 'nonce-...' 'strict-dynamic'; report-uri /api/security/csp-reports;
Pada endpoint backend /api/security/csp-reports, tim engineer dapat mencatat setiap log insiden dalam format JSON:
{
"csp-report": {
"document-uri": "https://puluhlabs.com/checkout",
"referrer": "",
"violated-directive": "script-src",
"effective-directive": "script-src",
"original-policy": "default-src 'self'...",
"disposition": "report",
"blocked-uri": "eval",
"line-number": 42,
"source-file": "https://thirdparty.example.com/widget.js"
}
}
Audit berkala terhadap data laporan ini memungkinkan tim mengidentifikasi ekstensi browser nakal pengguna versus potensi payload XSS nyata sebelum akhirnya beralih ke enforcement Content-Security-Policy aktif secara permanen.
Strategi Hash-Based CSP untuk Halaman Statis (SSG) dan Edge CDN #
Ketika aplikasi web Next.js dikompilasi menggunakan mode static export (output: 'export') atau di-host pada jaringan CDN edge statis seperti Cloudflare Pages, pembuatan token nonce secara dinamis per request HTTP tidak dapat dilakukan tanpa komputasi edge worker. Jika seluruh halaman di-cache secara permanen di edge, menyematkan nonce statis akan membatalkan prinsip dasar keamanan nonce, karena penyerang dapat membaca nilai nonce statis tersebut dan menyisipkannya pada tag skrip injeksi mereka.
Sebagai alternatif bagi arsitektur Server-Side Generation (SSG), pengembang dapat memanfaatkan verifikasi integritas berbasis Hash SHA-256.
Dalam model hash-based CSP:
- Setiap potongan skrip inline dihitung nilai ringkasan kriptografisnya (cryptographic digest) menggunakan algoritma SHA-256, SHA-384, atau SHA-512.
- Nilai hash yang dihasilkan kemudian didaftarkan secara eksplisit ke dalam header
script-src:httpContent-Security-Policy: script-src 'self' 'sha256-qznLcsROx4GACP2dm0UCK3NzsLUKKA8ISFix9KCJ38=' 'sha256-RFWzHiAcPvMWbUIJSio00mR8urke4IFS8ckMvdDw04o='; - Saat browser mengurai berkas HTML, browser akan menghitung nilai hash dari setiap blok skrip inline di dalam dokumen. Jika hash tersebut cocok dengan salah satu entri yang terdaftar pada header HTTP, skrip diizinkan berjalan. Jika konten skrip dimanipulasi walau hanya satu karakter spasi, browser langsung menolak eksekusinya.
Pendekatan hash-based ini sangat ideal bagi situs portal korporat, dokumentasi teknis, dan landing page statis karena memungkinkan cache HTTP berdurasi panjang (long-term immutable caching) pada jaringan CDN global tanpa memerlukan waktu eksekusi komputasi server tambahan pada setiap kunjungan pengguna.
