Client Security
Your React app stores bearer tokens on the client. A cross-site scripting (XSS) attack could steal those tokens and impersonate the user. The primary defence is a Content Security Policy (CSP) — an HTTP response header that tells the browser which scripts are allowed to run.
Recommended CSP
Set a Content-Security-Policy header on every response from your frontend server. A strong starting point:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM}' 'strict-dynamic' 'wasm-unsafe-eval';
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob:;
font-src 'self';
connect-src 'self' https://your-pandora-server.example;
worker-src 'self' blob:;
frame-ancestors 'none';
object-src 'none';
base-uri 'self'Key directives:
| Directive | Purpose |
|---|---|
script-src 'nonce-...' 'strict-dynamic' | Only scripts with the per-request nonce (and scripts they load) can execute. Blocks injected scripts from reading localStorage. |
connect-src 'self' <api-url> | Limits fetch / XHR to your own origin and the Pandora API. Replace with your actual API URL. |
frame-ancestors 'none' | Prevents your UI from being embedded in iframes (clickjacking protection). |
wasm-unsafe-eval | Required if your UI uses WebAssembly (e.g. syntax highlighting, animations). |
Generating a Nonce
A nonce is a random value generated per request on the server and attached to both the CSP header and every trusted <script> tag. Because attackers can’t predict the nonce, injected scripts won’t have it and the browser blocks them.
In a Next.js app, the typical approach is a proxy file:
import { type NextRequest, NextResponse } from 'next/server'
export function proxy(request: NextRequest) {
const nonce = Buffer.from(crypto.randomUUID()).toString('base64')
const csp = `script-src 'self' 'nonce-${nonce}' 'strict-dynamic' 'wasm-unsafe-eval'; ...`
const requestHeaders = new Headers(request.headers)
requestHeaders.set('x-nonce', nonce)
const response = NextResponse.next({ request: { headers: requestHeaders } })
response.headers.set('Content-Security-Policy', csp)
return response
}Then read the nonce in your root layout via headers() and pass it to any components that inject inline scripts (e.g. theme providers).
Additional Headers
These are not strictly CSP but provide further hardening:
| Header | Value | Purpose |
|---|---|---|
X-Content-Type-Options | nosniff | Prevents MIME-type sniffing |
X-Frame-Options | DENY | Legacy clickjacking protection (supplements frame-ancestors) |
Referrer-Policy | strict-origin-when-cross-origin | Limits referrer leakage |
Permissions-Policy | camera=(), microphone=(), geolocation=() | Disables unused browser APIs |