Going further
Content Security Policy
A Content Security Policy tells the browser which scripts it's allowed to run, and nestjs-mvc makes sure its own scripts pass yours.
Why a nonce
A strict policy blocks every script you haven't approved. You approve them with a nonce, which is a random value in the policy header that each script tag has to carry as well. Someone who injects a script doesn't know that value, so their script won't run.
Set the header with helmet
Use helmet, like the NestJS docs recommend, and get the nonce from nestjs-mvc:
// src/main.ts
import helmet from 'helmet'
import { nonce } from 'nestjs-mvc'
app.use(
helmet({
contentSecurityPolicy: {
directives: {
scriptSrc: ["'self'", (req) => `'nonce-${nonce(req)}'`],
styleSrc: ["'self'", "'unsafe-inline'"],
connectSrc: ["'self'", 'ws:'],
},
},
}),
)
Every script tag nestjs-mvc renders now carries that nonce, including the ones Vite adds while you're developing.
Scripts of your own
If you need an inline script in your template, use ctx.nonce:
export function template(page: PageObject, ctx: TemplateContext) {
return `<!DOCTYPE html>
<html>
<head>
${ctx.assets()}
<script nonce="${ctx.nonce}">window.appVersion = '1.4.0'</script>
</head>
<body>${ctx.body()}</body>
</html>`
}
Without helmet
If your policy is set somewhere else, like a proxy, turn the nonce on for every page:
MvcModule.forRoot({ vite: {}, csp: { nonce: true } })
Good to know
- Styles are trickier. Inline
styleattributes can't carry a nonce, so keep'unsafe-inline'for styles like in the example above, unless you never use inline styles. ws:is there for development. Vite uses a websocket to reload your pages when you save a file.- Don't cache HTML pages at a CDN when you use a nonce. A cached page has an old nonce that won't match the header anymore.