Anatomi Risiko XSS pada Inertia SSR
Aplikasi berbasis SPA hybrid seperti Inertia.js mengandalkan root template (umumnya app.blade.php) untuk memuat asset bundle dan payload inisialisasi state JSON via atribut data-page. Ketika server-side rendering (SSR) diaktifkan, Node.js runner merender initial HTML di server lalu mengirimkannya ke browser untuk dihidrasi.
Tanpa Content Security Policy (CSP) yang ketat, celah XSS dapat dimanfaatkan penyerang untuk menyuntikkan inline script jahat. Menggunakan directive usang 'unsafe-inline' pada script-src meniadakan perlindungan CSP terhadap injection. Solusi standarnya adalah menggunakan CSP Nonce (number used once)—string acak kriptografis yang digenerate per request HTTP, dikirimkan melalui header response, dan disematkan pada setiap tag <script> yang sah.
Tantangan utama pada stack Inertia SSR adalah memastikan bahwa nonce yang digenerate di backend Laravel tersinkronisasi ke asset loader Vite, root Blade directive, dan proses SSR hydration tanpa memicu mismatch error di browser.
Implementasi CSP Middleware di Laravel
Langkah pertama adalah membuat middleware untuk menghasilkan nonce acak, meregistrasikannya ke service container dan facade Vite, serta melampirkannya ke HTTP header response.
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Vite;
use Symfony\Component\HttpFoundation\Response;
class ContentSecurityPolicy
{
public function handle(Request $request, Closure $next): Response
{
$nonce = base64_encode(random_bytes(16));
// Registrasikan nonce ke container dan Vite facade
app()->instance('csp-nonce', $nonce);
Vite::useCspNonce($nonce);
/** @var Response $response */
$response = $next($request);
$policy = [
"default-src 'self'",
"script-src 'self' 'nonce-{$nonce}' 'strict-dynamic'",
"style-src 'self' 'nonce-{$nonce}' https://fonts.googleapis.com",
"font-src 'self' https://fonts.gstatic.com",
"img-src 'self' data: https:",
"connect-src 'self'",
"object-src 'none'",
"base-uri 'self'",
"form-action 'self'",
];
$response->headers->set('Content-Security-Policy', implode('; ', $policy));
return $response;
}
}
Daftarkan middleware tersebut pada pipeline global HTTP (misalnya di bootstrap/app.php untuk Laravel 11 atau app/Http/Kernel.php untuk Laravel versi sebelumnya):
->withMiddleware(function (Middleware $middleware) {
$middleware->web(append: [
\App\Http\Middleware\ContentSecurityPolicy::class,
]);
})
Penerapan Nonce pada Root View Blade
Vite di Laravel telah memiliki dukungan bawaan untuk CSP nonce via Vite::useCspNonce(). Directive @vite akan otomatis merender tag script dan style link dengan atribut nonce="...". Pada root template resources/views/app.blade.php, Anda harus memastikan directive Inertia menerima nonce yang sama.
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title inertia>{{ config('app.name', 'Laravel') }}</title>
{{-- Directive Vite otomatis menyertakan nonce via Vite::useCspNonce --}}
@vite(['resources/js/app.js', "resources/js/Pages/{$page['component']}.vue"])
{{-- Directive Inertia Head untuk SSR --}}
@inertiaHead(nonce: app('csp-nonce'))
</head>
<body class="font-sans antialiased">
@inertia
</body>
</html>
Catatan: Parameternoncepada@inertiaHeadmemastikan tag<style>atau meta dinamis yang dihasilkan server SSR membawa nonce yang valid sebelum di-parse oleh browser.
Sinkronisasi Nonce dan Pencegahan Hydration Mismatch
Hydration mismatch terjadi jika DOM yang dihasilkan oleh SSR runner di Node.js berbeda dengan apa yang coba dihidrasi oleh klien. Masalah umum pada CSP berbasis nonce adalah ketika script pihak ketiga di-inject pada SSR atau ketika klien merender tag dengan nonce hardcoded yang tidak cocok dengan nonce request saat ini.
Inertia secara otomatis menyertakan state halaman pada atribut data-page. Jika frontend framework (Vue/React) perlu menyuntikkan script dinamis secara programmatic saat runtime, lewatkan nonce melalui shared props via HandleInertiaRequests.php:
public function share(Request $request): array
{
return array_merge(parent::share($request), [
'cspNonce' => fn () => app()->bound('csp-nonce') ? app('csp-nonce') : null,
]);
}
Pada SSR bundle (resources/js/ssr.js), pastikan konfigurasi render tidak menambahkan elemen inline script kustom tanpa atribut nonce. Browser tidak mengecek atribut nonce pada konten HTML statis di dalam root container Inertia (komponen UI), melainkan hanya pada elemen <script> tingkat root dan modul yang dieksekusi oleh DOM parser browser.
Verifikasi dan Pengujian Keamanan
1. Periksa Header HTTP
Jalankan curl untuk memverifikasi bahwa header CSP terkirim bersama nonce unik pada setiap request:
curl -I http://localhost:8000
Pastikan output mengandung header CSP:
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-rAnD0mStRiNg123==' 'strict-dynamic'; ...
2. Uji Penolakan Inline Script
Buka DevTools di browser, lalu buka Console dan suntikkan script ilegal secara manual ke DOM:
const maliciousScript = document.createElement('script');
maliciousScript.innerHTML = "console.log('XSS Triggered!');";
document.body.appendChild(maliciousScript);
Browser wajib memblokir script tersebut dan menampilkan violation report di Console:
Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'self' 'nonce-...' 'strict-dynamic'". Either the 'unsafe-inline' keyword, a hash ('sha256-...'), or a nonce ('nonce-...') is required to enable inline execution.
3. Uji Validitas Bundle Script
Pastikan file bundle utama dari Vite berjalan normal tanpa error CSP di console. Elemen <script type="module" src="..." nonce="..."> harus memiliki nilai atribut nonce yang identik dengan token pada header response request HTTP tersebut.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!