From 42f603404846cec2a198b1897be8cf98de369b30 Mon Sep 17 00:00:00 2001 From: Abdelrhman Arnos Date: Sat, 16 Aug 2025 14:07:26 +0000 Subject: [PATCH 1/2] docs(csp): clarify usage of html.cspNonce and its implications for security --- docs/config/shared-options.md | 44 +++++++++++++++++++++++++++++++++++ docs/guide/features.md | 8 +++++++ 2 files changed, 52 insertions(+) diff --git a/docs/config/shared-options.md b/docs/config/shared-options.md index b2da75237150e8..2b3abb64beb59c 100644 --- a/docs/config/shared-options.md +++ b/docs/config/shared-options.md @@ -171,6 +171,50 @@ Enabling this setting causes vite to determine file identity by the original fil A nonce value placeholder that will be used when generating script / style tags. Setting this value will also generate a meta tag with nonce value. +::: tip This is a placeholder, not the runtime nonce +`html.cspNonce` must be a deterministic literal placeholder string (for example, `CSP_NONCE_PLACEHOLDER`), not a randomly generated value. The real, per-request random nonce needs to be generated by your HTTP server and substituted into every HTML response just before it is sent, replacing the placeholder in the HTML and added to your `Content-Security-Policy` response header (for example: `script-src 'self' 'nonce-'`). +::: + +### When to use (and not use) `html.cspNonce` + +Use this option when you have a server that can: + +1. Generate a fresh cryptographically strong random nonce on every request. +2. Inject the nonce into the CSP response header. +3. Replace the placeholder value emitted by Vite in the built HTML before sending it to the client. + +If you are deploying the built files as immutable static assets (for example on a static host / CDN with `Cache-Control: max-age=31536000, immutable`) and you do not have an HTML edge function or origin server performing on-the-fly substitution, a nonce based policy is usually impractical because the HTML would need to vary per request and therefore cannot be cached immutably. In that scenario prefer a hash-based CSP (hashes of the inline runtime snippets) or a policy that avoids inline code entirely. + +### Example server-side replacement (Node / Express style middleware) + +```js +// Why: generate a new nonce each request and replace the placeholder that Vite emitted at build time +const PLACEHOLDER = 'CSP_NONCE_PLACEHOLDER' + +app.use(async (req, res, next) => { + const nonce = crypto.randomBytes(16).toString('base64') + res.setHeader( + 'Content-Security-Policy', + `default-src 'self'; script-src 'self' 'nonce-${nonce}'; style-src 'self' 'nonce-${nonce}'`, + ) + let html = await fs.promises.readFile( + path.join(distDir, 'index.html'), + 'utf8', + ) + html = html.replaceAll(PLACEHOLDER, nonce) + res.setHeader('Content-Type', 'text/html') + res.end(html) +}) +``` + +### Static hosting & hashes + +If you cannot do per-request HTML mutation, consider a hash-based CSP. A community plugin (for example `vite-plugin-csp-guard`) demonstrates a hash workflow. Hash policies allow the HTML to stay byte-for-byte cacheable because the hash values are derived at build time and stay constant as long as the content does. + +### Summary + +`html.cspNonce` provides a coordination point (placeholder) so Vite can attach the runtime nonce to its injected tags. The security comes only after you implement correct server-side per-request replacement and header emission; using a fixed nonce baked into a static build defeats the protection and should be avoided. + ## css.modules - **Type:** diff --git a/docs/guide/features.md b/docs/guide/features.md index de101970d07536..ca02b912a55494 100644 --- a/docs/guide/features.md +++ b/docs/guide/features.md @@ -774,6 +774,14 @@ The nonce value of a meta tag with `property="csp-nonce"` will be used by Vite w Ensure that you replace the placeholder with a unique value for each request. This is important to prevent bypassing a resource's policy, which can otherwise be easily done. ::: +::: tip Placeholder vs runtime nonce +`html.cspNonce` must be a stable placeholder string (e.g. `CSP_NONCE_PLACEHOLDER`). Your server (or edge middleware) is responsible for generating a cryptographically strong random nonce per request, inserting it into the `Content-Security-Policy` header, and replacing every occurrence of the placeholder in the HTML before sending it. Do not configure Vite with a pre-generated random value; a build-time nonce reused across requests provides no protection and creates a false sense of security. +::: + +::: info Static hosting limitations +If you deploy pure static files (e.g. to an object store/CDN with immutable caching) and cannot mutate HTML per request, a nonce-based CSP is usually incompatible because the HTML would need to vary per request to carry the fresh nonce. In such cases use a hash-based CSP (hashes of inline snippets) or remove inline code. Community plugins like `vite-plugin-csp-guard` illustrate a hash approach. See the additional guidance in [`html.cspNonce` docs](/config/shared-options#htmlcspnonce). +::: + ### [`data:`]() By default, during build, Vite inlines small assets as data URIs. Allowing `data:` for related directives (e.g. [`img-src`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/img-src), [`font-src`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/font-src)), or, disabling it by setting [`build.assetsInlineLimit: 0`](/config/build-options#build-assetsinlinelimit) is necessary. From 1f6dab1cc4a4666e1dedbb7637e0e595918f0b6b Mon Sep 17 00:00:00 2001 From: Abdelrhman Arnos Date: Tue, 19 Aug 2025 11:43:17 +0200 Subject: [PATCH 2/2] docs(csp): remove example code and clarify nonce usage in CSP configuration --- docs/config/shared-options.md | 22 ---------------------- docs/guide/features.md | 10 ++-------- 2 files changed, 2 insertions(+), 30 deletions(-) diff --git a/docs/config/shared-options.md b/docs/config/shared-options.md index 2b3abb64beb59c..19a76ad0652840 100644 --- a/docs/config/shared-options.md +++ b/docs/config/shared-options.md @@ -185,28 +185,6 @@ Use this option when you have a server that can: If you are deploying the built files as immutable static assets (for example on a static host / CDN with `Cache-Control: max-age=31536000, immutable`) and you do not have an HTML edge function or origin server performing on-the-fly substitution, a nonce based policy is usually impractical because the HTML would need to vary per request and therefore cannot be cached immutably. In that scenario prefer a hash-based CSP (hashes of the inline runtime snippets) or a policy that avoids inline code entirely. -### Example server-side replacement (Node / Express style middleware) - -```js -// Why: generate a new nonce each request and replace the placeholder that Vite emitted at build time -const PLACEHOLDER = 'CSP_NONCE_PLACEHOLDER' - -app.use(async (req, res, next) => { - const nonce = crypto.randomBytes(16).toString('base64') - res.setHeader( - 'Content-Security-Policy', - `default-src 'self'; script-src 'self' 'nonce-${nonce}'; style-src 'self' 'nonce-${nonce}'`, - ) - let html = await fs.promises.readFile( - path.join(distDir, 'index.html'), - 'utf8', - ) - html = html.replaceAll(PLACEHOLDER, nonce) - res.setHeader('Content-Type', 'text/html') - res.end(html) -}) -``` - ### Static hosting & hashes If you cannot do per-request HTML mutation, consider a hash-based CSP. A community plugin (for example `vite-plugin-csp-guard`) demonstrates a hash workflow. Hash policies allow the HTML to stay byte-for-byte cacheable because the hash values are derived at build time and stay constant as long as the content does. diff --git a/docs/guide/features.md b/docs/guide/features.md index ca02b912a55494..8c7f626680e08e 100644 --- a/docs/guide/features.md +++ b/docs/guide/features.md @@ -768,14 +768,8 @@ To deploy CSP, certain directives or configs must be set due to Vite's internals When [`html.cspNonce`](/config/shared-options#html-cspnonce) is set, Vite adds a nonce attribute with the specified value to any `