Configure CSP
Configure Content Security Policy headers, nonce for inline script, and overall suggestions.
Content Security Policy (CSP) is important to guard your Waku application against various security threats such as cross-site scripting (XSS), clickjacking, and other code injection attacks.
By using CSP, developers can specify which origins are permissible for content sources, scripts, stylesheets, images, fonts, objects, media (audio, video), iframes, and more.
Inline Script and nonce
Waku relies on inline scripts for basic functionality: loading client entry module, streaming inital RSC workload, and prefetching assets on navigation.
Inline script is also a source of Cross-site scripting (XSS). To mitigate this, a common practice is to set nonce attribute on <script>, together with proper Content-Security-Policy response header.
Since Waku v1.0.0-alpha.3, introduced in PR#1922, Waku has built-in support with nonce.
Waku has 2 ways to set nonce.
Hono Middleware
Hono provides a middleware which simplifies the setup of security headers Set up a middleware in src/middleware/nonce.ts:
import type { MiddlewareHandler } from 'hono';
import { NONCE, secureHeaders } from 'hono/secure-headers';
import { unstable_getContext as getContext } from 'waku/server';
const nonceMiddleware = (): MiddlewareHandler => {
const secure = secureHeaders({
contentSecurityPolicy: {
scriptSrc: ["'self'", NONCE],
},
});
return async (c, next) => {
await secure(c, async () => {
// Bridge Hono's nonce to Waku
const nonce = c.get('secureHeadersNonce');
if (nonce) {
const context = getContext();
context.nonce = nonce;
}
await next();
});
};
};
export default nonceMiddleware;Behind the scenes, hono generates the nonce, passes it to Content-Security-Policy headers (you can add more here) and stores it in secureHeadersNonce context. We need to pass it again to Waku context so Waku can apply it to its inline script.
Pass nonce in waku.server.entry
If your adapter allows you to customize handleRequest, you can pass it manually:
import adapter from 'waku/adapters/default';
import { Slot } from 'waku/minimal/client';
import App from './components/App.js';
import { encodeBase64 } from 'hono/utils/encode';
const generateNonce = () => {
const arrayBuffer = new Uint8Array(16);
crypto.getRandomValues(arrayBuffer);
return 'toBase64' in arrayBuffer
? arrayBuffer.toBase64()
: encodeBase64(arrayBuffer.buffer);
};
const NONCE = generateNonce();
export default adapter({
handleRequest: async (input, { renderRsc, renderHtml }) => {
if (input.type === 'component') {
return renderRsc({ App: <App /> });
}
if (input.type === 'custom' && input.pathname === '/') {
const response = await renderHtml(
await renderRsc({ App: <App /> }),
<Slot id="App" />,
{
rscPath: '',
nonce: NONCE,
},
);
// Set CSP header with the nonce
response.headers.set(
'Content-Security-Policy',
`script-src 'self' 'nonce-${NONCE}';`,
);
return response;
}
},
handleBuild: async () => {},
});We set a minimal CSP header here. In practice, CSP should be as minimal or strict as possible. We recommend hono middleware approach as it provides a good default value.
Limitation
In the case of SSG, a nonce is not secure because it must be randomly generated for every response. Since SSG produces static HTML, this is not possible. If your security requirements prioritize a nonce over static HTML benefits, consider disabling HTML pre-rendering, or intercept HTML stream if your host platform provide similar middleware mechanism (e.g. Netlify's Edge Function).

