From adaad77bae28432ce7262c393111ff3baa6d7026 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Wed, 28 Feb 2024 23:46:10 +0900 Subject: [PATCH 01/14] feat: `html.cspNonce` option Co-authored-by: Andrew <8158705+maccuaa@users.noreply.github.com> --- packages/plugin-legacy/src/index.ts | 13 +++- packages/vite/src/node/config.ts | 13 ++++ packages/vite/src/node/index.ts | 1 + packages/vite/src/node/plugins/html.ts | 24 +++++-- .../src/node/server/middlewares/indexHtml.ts | 29 +++++++- playground/csp/__tests__/csp.spec.ts | 14 ++++ playground/csp/index.html | 10 +++ playground/csp/index.js | 1 + playground/csp/linked.css | 3 + playground/csp/package.json | 12 ++++ playground/csp/vite.config.js | 67 +++++++++++++++++++ pnpm-lock.yaml | 2 + 12 files changed, 180 insertions(+), 9 deletions(-) create mode 100644 playground/csp/__tests__/csp.spec.ts create mode 100644 playground/csp/index.html create mode 100644 playground/csp/index.js create mode 100644 playground/csp/linked.css create mode 100644 playground/csp/package.json create mode 100644 playground/csp/vite.config.js diff --git a/packages/plugin-legacy/src/index.ts b/packages/plugin-legacy/src/index.ts index b47d47ffc7801f..fa30c81bf29d0a 100644 --- a/packages/plugin-legacy/src/index.ts +++ b/packages/plugin-legacy/src/index.ts @@ -522,6 +522,7 @@ function viteLegacyPlugin(options: Options = {}): Plugin[] { const tags: HtmlTagDescriptor[] = [] const htmlFilename = chunk.facadeModuleId?.replace(/\?.*$/, '') + const cspNonce = config.html?.cspNonce // 1. inject modern polyfills if (genModern) { @@ -540,6 +541,7 @@ function viteLegacyPlugin(options: Options = {}): Plugin[] { chunk.facadeModuleId!, config, ), + ...(cspNonce ? { nonce: cspNonce } : {}), }, }) } else if (modernPolyfills.size) { @@ -557,7 +559,10 @@ function viteLegacyPlugin(options: Options = {}): Plugin[] { if (genModern) { tags.push({ tag: 'script', - attrs: { nomodule: genModern }, + attrs: { + nomodule: genModern, + ...(cspNonce ? { nonce: cspNonce } : {}), + }, children: safari10NoModuleFix, injectTo: 'body', }) @@ -579,6 +584,7 @@ function viteLegacyPlugin(options: Options = {}): Plugin[] { chunk.facadeModuleId!, config, ), + ...(cspNonce ? { nonce: cspNonce } : {}), }, injectTo: 'body', }) @@ -608,6 +614,7 @@ function viteLegacyPlugin(options: Options = {}): Plugin[] { chunk.facadeModuleId!, config, ), + ...(cspNonce ? { nonce: cspNonce } : {}), }, children: systemJSInlineCode, injectTo: 'body', @@ -622,13 +629,13 @@ function viteLegacyPlugin(options: Options = {}): Plugin[] { if (legacyPolyfillFilename && legacyEntryFilename && genModern) { tags.push({ tag: 'script', - attrs: { type: 'module' }, + attrs: { type: 'module', ...(cspNonce ? { nonce: cspNonce } : {}) }, children: detectModernBrowserCode, injectTo: 'head', }) tags.push({ tag: 'script', - attrs: { type: 'module' }, + attrs: { type: 'module', ...(cspNonce ? { nonce: cspNonce } : {}) }, children: dynamicFallbackInlineCode, injectTo: 'head', }) diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index 2e18cc8b30ba10..2002f4b822859b 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -173,6 +173,10 @@ export interface UserConfig { * Configure resolver */ resolve?: ResolveOptions & { alias?: AliasOptions } + /** + * HTML related options + */ + html?: HTMLOptions /** * CSS related options (preprocessors and CSS modules) */ @@ -281,6 +285,15 @@ export interface UserConfig { appType?: AppType } +export interface HTMLOptions { + /** + * A nonce value placeholder that will be used when generating script/style tags. + * + * Make sure that this placeholder will be replaced with a unique value for each request by the server. + */ + cspNonce?: string +} + export interface ExperimentalOptions { /** * Append fake `&lang.(ext)` when queries are specified, to preserve the file extension for following plugins to process. diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index d640f51d31939e..3b84c34b0626a8 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -24,6 +24,7 @@ export type { AppType, ConfigEnv, ExperimentalOptions, + HTMLOptions, InlineConfig, LegacyOptions, PluginHookUtils, diff --git a/packages/vite/src/node/plugins/html.ts b/packages/vite/src/node/plugins/html.ts index eb7973296af86b..0a66015e49d785 100644 --- a/packages/vite/src/node/plugins/html.ts +++ b/packages/vite/src/node/plugins/html.ts @@ -543,11 +543,9 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { node.attrs.some( (p) => p.name === 'rel' && - p.value - .split(spaceRe) - .some((v) => - noInlineLinkRels.has(v.toLowerCase()), - ), + parseRelAttr(p.value).some((v) => + noInlineLinkRels.has(v), + ), ) const shouldInline = isNoInlineLink ? false : undefined assetUrlsPromises.push( @@ -608,6 +606,13 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { styleNode.sourceCodeLocation!.endOffset, `__VITE_INLINE_CSS__${hash}_${inlineModuleIndex}__`, ) + + if (config.html?.cspNonce) { + s.appendRight( + node.sourceCodeLocation!.startTag!.endOffset - 1, + ` nonce="${config.html.cspNonce}"`, + ) + } } if (shouldRemove) { @@ -677,6 +682,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { }, async generateBundle(options, bundle) { + const cspNonce = config.html?.cspNonce const analyzedChunk: Map = new Map() const inlineEntryChunk = new Set() const getImportedChunks = ( @@ -714,6 +720,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { // Now ``, + ``, ) preTransformRequest(server!, modulePath, base) } @@ -272,6 +274,10 @@ const devHtmlHook: IndexHtmlTransformHook = async ( if (processedUrl !== src.value) { overwriteAttrValue(s, sourceCodeLocation!, processedUrl) } + + if (cspNonce) { + s.appendRight(sourceCodeLocation!.endOffset, ` nonce="${cspNonce}"`) + } } else if (isModule && node.childNodes.length) { addInlineModule(node, 'js') } else if (node.childNodes.length) { @@ -297,6 +303,21 @@ const devHtmlHook: IndexHtmlTransformHook = async ( } } + if ( + cspNonce && + node.nodeName === 'link' && + node.attrs.some( + (attr) => + attr.name === 'rel' && + parseRelAttr(attr.value).includes('stylesheet'), + ) + ) { + s.appendRight( + node.sourceCodeLocation!.attrs!['rel'].endOffset, + ` nonce="${cspNonce}"`, + ) + } + const inlineStyle = findNeedTransformStyleAttribute(node) if (inlineStyle) { inlineModuleIndex++ @@ -314,6 +335,11 @@ const devHtmlHook: IndexHtmlTransformHook = async ( end: children.sourceCodeLocation!.endOffset, code: children.value, }) + + s.appendRight( + node.sourceCodeLocation!.startTag!.endOffset - 1, + ` nonce="${cspNonce}"`, + ) } // elements with [href/src] attrs @@ -392,6 +418,7 @@ const devHtmlHook: IndexHtmlTransformHook = async ( attrs: { type: 'module', src: path.posix.join(base, CLIENT_PUBLIC_PATH), + ...(cspNonce ? { nonce: cspNonce } : {}), }, injectTo: 'head-prepend', }, diff --git a/playground/csp/__tests__/csp.spec.ts b/playground/csp/__tests__/csp.spec.ts new file mode 100644 index 00000000000000..a2689e4e7f7df3 --- /dev/null +++ b/playground/csp/__tests__/csp.spec.ts @@ -0,0 +1,14 @@ +import { expect, test } from 'vitest' +import { expectWithRetry, getColor, page } from '~utils' + +test('linked css', async () => { + expect(await getColor('.linked')).toBe('blue') +}) + +test('inline style tag', async () => { + expect(await getColor('.inline')).toBe('green') +}) + +test('script tag', async () => { + await expectWithRetry(() => page.textContent('.js')).toBe('js: ok') +}) diff --git a/playground/csp/index.html b/playground/csp/index.html new file mode 100644 index 00000000000000..682dcab2778f23 --- /dev/null +++ b/playground/csp/index.html @@ -0,0 +1,10 @@ + + + +

direct

+

inline

+

js: error

diff --git a/playground/csp/index.js b/playground/csp/index.js new file mode 100644 index 00000000000000..88c2e1d38fc06e --- /dev/null +++ b/playground/csp/index.js @@ -0,0 +1 @@ +document.querySelector('.js').textContent = 'js: ok' diff --git a/playground/csp/linked.css b/playground/csp/linked.css new file mode 100644 index 00000000000000..51636e6cfad81f --- /dev/null +++ b/playground/csp/linked.css @@ -0,0 +1,3 @@ +.linked { + color: blue; +} diff --git a/playground/csp/package.json b/playground/csp/package.json new file mode 100644 index 00000000000000..e8a834d93abd25 --- /dev/null +++ b/playground/csp/package.json @@ -0,0 +1,12 @@ +{ + "name": "@vitejs/test-csp", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "debug": "node --inspect-brk ../../packages/vite/bin/vite", + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + } +} diff --git a/playground/csp/vite.config.js b/playground/csp/vite.config.js new file mode 100644 index 00000000000000..08d2b74f9dde3c --- /dev/null +++ b/playground/csp/vite.config.js @@ -0,0 +1,67 @@ +import fs from 'node:fs/promises' +import url from 'node:url' +import path from 'node:path' +import crypto from 'node:crypto' +import { defineConfig } from 'vite' + +const __dirname = path.dirname(url.fileURLToPath(import.meta.url)) + +const noncePlaceholder = '#$NONCE$#' +const createNonce = () => crypto.randomBytes(16).toString('base64') + +/** + * @param {import('node:http').ServerResponse} res + * @param {string} nonce + */ +const setNonceHeader = (res, nonce) => { + res.setHeader( + 'Content-Security-Policy', + `default-src 'nonce-${nonce}'; connect-src 'self'`, + ) +} + +/** + * @param {string} file + * @param {(input: string, originalUrl: string) => Promise} transform + * @returns {import('vite').Connect.NextHandleFunction} + */ +const createMiddleware = (file, transform) => async (req, res) => { + const nonce = createNonce() + setNonceHeader(res, nonce) + const content = await fs.readFile(path.join(__dirname, file), 'utf8') + const transformedContent = await transform(content, req.originalUrl) + res.setHeader('Content-Type', 'text/html') + res.end(transformedContent.replaceAll(noncePlaceholder, nonce)) +} + +export default defineConfig({ + plugins: [ + { + name: 'nonce-inject', + config() { + return { + appType: 'custom', + html: { + cspNonce: noncePlaceholder, + }, + } + }, + configureServer({ transformIndexHtml, middlewares }) { + return () => { + middlewares.use( + createMiddleware('./index.html', (input, originalUrl) => + transformIndexHtml(originalUrl, input), + ), + ) + } + }, + configurePreviewServer({ middlewares }) { + return () => { + middlewares.use( + createMiddleware('./dist/index.html', async (input) => input), + ) + } + }, + }, + ], +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 55b121fdf7fd53..c981cf10b3f944 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -539,6 +539,8 @@ importers: specifier: ^4.17.21 version: 4.17.21 + playground/csp: {} + playground/css: devDependencies: '@vitejs/test-css-dep': From 9fea3652de5b999481dc2ec986c775775e87411c Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Thu, 29 Feb 2024 00:00:27 +0900 Subject: [PATCH 02/14] feat: inject `meta[property=csp-nonce]` --- packages/vite/src/node/plugins/html.ts | 17 +++++++++++++++++ .../src/node/server/middlewares/indexHtml.ts | 2 ++ playground/csp/__tests__/csp.spec.ts | 5 +++++ 3 files changed, 24 insertions(+) diff --git a/packages/vite/src/node/plugins/html.ts b/packages/vite/src/node/plugins/html.ts index 0a66015e49d785..22376551980ab0 100644 --- a/packages/vite/src/node/plugins/html.ts +++ b/packages/vite/src/node/plugins/html.ts @@ -308,6 +308,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { config.plugins, config.logger, ) + preHooks.unshift(injectCspNonceMetaTagHook(config)) preHooks.unshift(preImportMapHook(config)) preHooks.push(htmlEnvHook(config)) postHooks.push(postImportMapHook()) @@ -1093,6 +1094,22 @@ export function postImportMapHook(): IndexHtmlTransformHook { } } +export function injectCspNonceMetaTagHook( + config: ResolvedConfig, +): IndexHtmlTransformHook { + return () => { + if (!config.html?.cspNonce) return + + return [ + { + tag: 'meta', + injectTo: 'head', + attrs: { property: 'csp-nonce', nonce: config.html.cspNonce }, + }, + ] + } +} + /** * Support `%ENV_NAME%` syntax in html files */ diff --git a/packages/vite/src/node/server/middlewares/indexHtml.ts b/packages/vite/src/node/server/middlewares/indexHtml.ts index b835e74da126aa..08b045c42655f0 100644 --- a/packages/vite/src/node/server/middlewares/indexHtml.ts +++ b/packages/vite/src/node/server/middlewares/indexHtml.ts @@ -15,6 +15,7 @@ import { getScriptInfo, htmlEnvHook, htmlProxyResult, + injectCspNonceMetaTagHook, nodeIsElement, overwriteAttrValue, parseRelAttr, @@ -70,6 +71,7 @@ export function createDevHtmlTransformFn( ) const transformHooks = [ preImportMapHook(config), + injectCspNonceMetaTagHook(config), ...preHooks, htmlEnvHook(config), devHtmlHook, diff --git a/playground/csp/__tests__/csp.spec.ts b/playground/csp/__tests__/csp.spec.ts index a2689e4e7f7df3..f70cdf45965fc8 100644 --- a/playground/csp/__tests__/csp.spec.ts +++ b/playground/csp/__tests__/csp.spec.ts @@ -12,3 +12,8 @@ test('inline style tag', async () => { test('script tag', async () => { await expectWithRetry(() => page.textContent('.js')).toBe('js: ok') }) + +test('meta[property=csp-nonce] is injected', async () => { + const meta = await page.$('meta[property=csp-nonce]') + expect(await (await meta.getProperty('nonce')).jsonValue()).not.toBe('') +}) From 0d0a47824c3610541983dba514acc8f102b7126b Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Thu, 29 Feb 2024 00:10:23 +0900 Subject: [PATCH 03/14] feat: add nonce when injecting style tag in dev Co-Authored-By: Justin Tay <49700559+justin-tay@users.noreply.github.com> --- packages/vite/src/client/client.ts | 8 ++++++++ playground/csp/__tests__/csp.spec.ts | 4 ++++ playground/csp/from-js.css | 3 +++ playground/csp/index.html | 1 + playground/csp/index.js | 2 ++ 5 files changed, 18 insertions(+) create mode 100644 playground/csp/from-js.css diff --git a/packages/vite/src/client/client.ts b/packages/vite/src/client/client.ts index ec29331085a010..f2fa9a4309a310 100644 --- a/packages/vite/src/client/client.ts +++ b/packages/vite/src/client/client.ts @@ -383,6 +383,11 @@ if ('document' in globalThis) { }) } +const cspNonce = + 'document' in globalThis + ? document.querySelector('meta[property=csp-nonce]')?.nonce + : undefined + // all css imports should be inserted at the same position // because after build it will be a single css file let lastInsertedStyle: HTMLStyleElement | undefined @@ -394,6 +399,9 @@ export function updateStyle(id: string, content: string): void { style.setAttribute('type', 'text/css') style.setAttribute('data-vite-dev-id', id) style.textContent = content + if (cspNonce) { + style.setAttribute('nonce', cspNonce) + } if (!lastInsertedStyle) { document.head.appendChild(style) diff --git a/playground/csp/__tests__/csp.spec.ts b/playground/csp/__tests__/csp.spec.ts index f70cdf45965fc8..45c10353e10446 100644 --- a/playground/csp/__tests__/csp.spec.ts +++ b/playground/csp/__tests__/csp.spec.ts @@ -9,6 +9,10 @@ test('inline style tag', async () => { expect(await getColor('.inline')).toBe('green') }) +test('imported css', async () => { + expect(await getColor('.from-js')).toBe('blue') +}) + test('script tag', async () => { await expectWithRetry(() => page.textContent('.js')).toBe('js: ok') }) diff --git a/playground/csp/from-js.css b/playground/csp/from-js.css new file mode 100644 index 00000000000000..fb48429dc60ab4 --- /dev/null +++ b/playground/csp/from-js.css @@ -0,0 +1,3 @@ +.from-js { + color: blue; +} diff --git a/playground/csp/index.html b/playground/csp/index.html index 682dcab2778f23..d29bd0fa94fab2 100644 --- a/playground/csp/index.html +++ b/playground/csp/index.html @@ -7,4 +7,5 @@

direct

inline

+

from-js

js: error

diff --git a/playground/csp/index.js b/playground/csp/index.js index 88c2e1d38fc06e..93bdbd12449580 100644 --- a/playground/csp/index.js +++ b/playground/csp/index.js @@ -1 +1,3 @@ +import './from-js.css' + document.querySelector('.js').textContent = 'js: ok' From 8e509738c3c665a4525def0018d51f8da08c72a6 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Thu, 29 Feb 2024 00:28:50 +0900 Subject: [PATCH 04/14] feat: use nonce value in meta tag for preload --- packages/vite/src/node/plugins/importAnalysisBuild.ts | 6 ++++++ playground/csp/__tests__/csp.spec.ts | 10 ++++++++++ playground/csp/dynamic.css | 3 +++ playground/csp/dynamic.js | 3 +++ playground/csp/index.html | 2 ++ playground/csp/index.js | 2 ++ 6 files changed, 26 insertions(+) create mode 100644 playground/csp/dynamic.css create mode 100644 playground/csp/dynamic.js diff --git a/packages/vite/src/node/plugins/importAnalysisBuild.ts b/packages/vite/src/node/plugins/importAnalysisBuild.ts index 2311f0859ef4d3..11a74d0ef0c620 100644 --- a/packages/vite/src/node/plugins/importAnalysisBuild.ts +++ b/packages/vite/src/node/plugins/importAnalysisBuild.ts @@ -80,6 +80,9 @@ function preload( // @ts-expect-error __VITE_IS_MODERN__ will be replaced with boolean later if (__VITE_IS_MODERN__ && deps && deps.length > 0) { const links = document.getElementsByTagName('link') + const cspNonce = document.querySelector( + 'meta[property=csp-nonce]', + )?.nonce promise = Promise.all( deps.map((dep) => { @@ -116,6 +119,9 @@ function preload( link.crossOrigin = '' } link.href = dep + if (cspNonce) { + link.setAttribute('nonce', cspNonce) + } document.head.appendChild(link) if (isCss) { return new Promise((res, rej) => { diff --git a/playground/csp/__tests__/csp.spec.ts b/playground/csp/__tests__/csp.spec.ts index 45c10353e10446..49155665a4143f 100644 --- a/playground/csp/__tests__/csp.spec.ts +++ b/playground/csp/__tests__/csp.spec.ts @@ -13,10 +13,20 @@ test('imported css', async () => { expect(await getColor('.from-js')).toBe('blue') }) +test('dynamic css', async () => { + expect(await getColor('.dynamic')).toBe('red') +}) + test('script tag', async () => { await expectWithRetry(() => page.textContent('.js')).toBe('js: ok') }) +test('dynamic js', async () => { + await expectWithRetry(() => page.textContent('.dynamic-js')).toBe( + 'dynamic-js: ok', + ) +}) + test('meta[property=csp-nonce] is injected', async () => { const meta = await page.$('meta[property=csp-nonce]') expect(await (await meta.getProperty('nonce')).jsonValue()).not.toBe('') diff --git a/playground/csp/dynamic.css b/playground/csp/dynamic.css new file mode 100644 index 00000000000000..ca5140e1c23d94 --- /dev/null +++ b/playground/csp/dynamic.css @@ -0,0 +1,3 @@ +.dynamic { + color: red; +} diff --git a/playground/csp/dynamic.js b/playground/csp/dynamic.js new file mode 100644 index 00000000000000..3d3e3a413e5677 --- /dev/null +++ b/playground/csp/dynamic.js @@ -0,0 +1,3 @@ +import './dynamic.css' + +document.querySelector('.dynamic-js').textContent = 'dynamic-js: ok' diff --git a/playground/csp/index.html b/playground/csp/index.html index d29bd0fa94fab2..7196d0a436bc8b 100644 --- a/playground/csp/index.html +++ b/playground/csp/index.html @@ -8,4 +8,6 @@

direct

inline

from-js

+

dynamic

js: error

+

dynamic-js: error

diff --git a/playground/csp/index.js b/playground/csp/index.js index 93bdbd12449580..465359baca8297 100644 --- a/playground/csp/index.js +++ b/playground/csp/index.js @@ -1,3 +1,5 @@ import './from-js.css' document.querySelector('.js').textContent = 'js: ok' + +import('./dynamic.js') From e973af0027ba68d774709b2976dd0d4433aff878 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Thu, 29 Feb 2024 01:19:40 +0900 Subject: [PATCH 05/14] refactor: inject nonce attribute in a separate html hook --- packages/plugin-legacy/src/index.ts | 9 +--- packages/vite/src/node/plugins/html.ts | 50 ++++++++++++++----- .../src/node/server/middlewares/indexHtml.ts | 31 ++---------- 3 files changed, 43 insertions(+), 47 deletions(-) diff --git a/packages/plugin-legacy/src/index.ts b/packages/plugin-legacy/src/index.ts index fa30c81bf29d0a..d6752411cf2da8 100644 --- a/packages/plugin-legacy/src/index.ts +++ b/packages/plugin-legacy/src/index.ts @@ -522,7 +522,6 @@ function viteLegacyPlugin(options: Options = {}): Plugin[] { const tags: HtmlTagDescriptor[] = [] const htmlFilename = chunk.facadeModuleId?.replace(/\?.*$/, '') - const cspNonce = config.html?.cspNonce // 1. inject modern polyfills if (genModern) { @@ -541,7 +540,6 @@ function viteLegacyPlugin(options: Options = {}): Plugin[] { chunk.facadeModuleId!, config, ), - ...(cspNonce ? { nonce: cspNonce } : {}), }, }) } else if (modernPolyfills.size) { @@ -561,7 +559,6 @@ function viteLegacyPlugin(options: Options = {}): Plugin[] { tag: 'script', attrs: { nomodule: genModern, - ...(cspNonce ? { nonce: cspNonce } : {}), }, children: safari10NoModuleFix, injectTo: 'body', @@ -584,7 +581,6 @@ function viteLegacyPlugin(options: Options = {}): Plugin[] { chunk.facadeModuleId!, config, ), - ...(cspNonce ? { nonce: cspNonce } : {}), }, injectTo: 'body', }) @@ -614,7 +610,6 @@ function viteLegacyPlugin(options: Options = {}): Plugin[] { chunk.facadeModuleId!, config, ), - ...(cspNonce ? { nonce: cspNonce } : {}), }, children: systemJSInlineCode, injectTo: 'body', @@ -629,13 +624,13 @@ function viteLegacyPlugin(options: Options = {}): Plugin[] { if (legacyPolyfillFilename && legacyEntryFilename && genModern) { tags.push({ tag: 'script', - attrs: { type: 'module', ...(cspNonce ? { nonce: cspNonce } : {}) }, + attrs: { type: 'module' }, children: detectModernBrowserCode, injectTo: 'head', }) tags.push({ tag: 'script', - attrs: { type: 'module', ...(cspNonce ? { nonce: cspNonce } : {}) }, + attrs: { type: 'module' }, children: dynamicFallbackInlineCode, injectTo: 'head', }) diff --git a/packages/vite/src/node/plugins/html.ts b/packages/vite/src/node/plugins/html.ts index 22376551980ab0..41f2df86fa1457 100644 --- a/packages/vite/src/node/plugins/html.ts +++ b/packages/vite/src/node/plugins/html.ts @@ -311,6 +311,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { preHooks.unshift(injectCspNonceMetaTagHook(config)) preHooks.unshift(preImportMapHook(config)) preHooks.push(htmlEnvHook(config)) + postHooks.push(injectNonceAttributeTagHook(config)) postHooks.push(postImportMapHook()) const processedHtml = new Map() @@ -607,13 +608,6 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { styleNode.sourceCodeLocation!.endOffset, `__VITE_INLINE_CSS__${hash}_${inlineModuleIndex}__`, ) - - if (config.html?.cspNonce) { - s.appendRight( - node.sourceCodeLocation!.startTag!.endOffset - 1, - ` nonce="${config.html.cspNonce}"`, - ) - } } if (shouldRemove) { @@ -683,7 +677,6 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { }, async generateBundle(options, bundle) { - const cspNonce = config.html?.cspNonce const analyzedChunk: Map = new Map() const inlineEntryChunk = new Set() const getImportedChunks = ( @@ -721,7 +714,6 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { // Now ``, + ``, ) preTransformRequest(server!, modulePath, base) } @@ -276,10 +276,6 @@ const devHtmlHook: IndexHtmlTransformHook = async ( if (processedUrl !== src.value) { overwriteAttrValue(s, sourceCodeLocation!, processedUrl) } - - if (cspNonce) { - s.appendRight(sourceCodeLocation!.endOffset, ` nonce="${cspNonce}"`) - } } else if (isModule && node.childNodes.length) { addInlineModule(node, 'js') } else if (node.childNodes.length) { @@ -305,21 +301,6 @@ const devHtmlHook: IndexHtmlTransformHook = async ( } } - if ( - cspNonce && - node.nodeName === 'link' && - node.attrs.some( - (attr) => - attr.name === 'rel' && - parseRelAttr(attr.value).includes('stylesheet'), - ) - ) { - s.appendRight( - node.sourceCodeLocation!.attrs!['rel'].endOffset, - ` nonce="${cspNonce}"`, - ) - } - const inlineStyle = findNeedTransformStyleAttribute(node) if (inlineStyle) { inlineModuleIndex++ @@ -337,11 +318,6 @@ const devHtmlHook: IndexHtmlTransformHook = async ( end: children.sourceCodeLocation!.endOffset, code: children.value, }) - - s.appendRight( - node.sourceCodeLocation!.startTag!.endOffset - 1, - ` nonce="${cspNonce}"`, - ) } // elements with [href/src] attrs @@ -420,7 +396,6 @@ const devHtmlHook: IndexHtmlTransformHook = async ( attrs: { type: 'module', src: path.posix.join(base, CLIENT_PUBLIC_PATH), - ...(cspNonce ? { nonce: cspNonce } : {}), }, injectTo: 'head-prepend', }, From 44320b0d4094be814142c1bfafd8f6bc85d092f0 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Thu, 29 Feb 2024 01:22:43 +0900 Subject: [PATCH 06/14] chore: reduce diff --- packages/plugin-legacy/src/index.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/plugin-legacy/src/index.ts b/packages/plugin-legacy/src/index.ts index d6752411cf2da8..b47d47ffc7801f 100644 --- a/packages/plugin-legacy/src/index.ts +++ b/packages/plugin-legacy/src/index.ts @@ -557,9 +557,7 @@ function viteLegacyPlugin(options: Options = {}): Plugin[] { if (genModern) { tags.push({ tag: 'script', - attrs: { - nomodule: genModern, - }, + attrs: { nomodule: genModern }, children: safari10NoModuleFix, injectTo: 'body', }) From 9e32652a84e7d2f7a5a324f69254f4dfaea9c6f9 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Thu, 29 Feb 2024 01:22:57 +0900 Subject: [PATCH 07/14] chore: add comment about nonce attribute --- packages/vite/src/node/plugins/html.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/vite/src/node/plugins/html.ts b/packages/vite/src/node/plugins/html.ts index 41f2df86fa1457..087ca95675890f 100644 --- a/packages/vite/src/node/plugins/html.ts +++ b/packages/vite/src/node/plugins/html.ts @@ -1093,6 +1093,8 @@ export function injectCspNonceMetaTagHook( { tag: 'meta', injectTo: 'head', + // use nonce attribute so that it's hidden + // https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/nonce#accessing_nonces_and_nonce_hiding attrs: { property: 'csp-nonce', nonce: config.html.cspNonce }, }, ] From d17074be609e01e58239e7dc0e43da39c81589b5 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Thu, 29 Feb 2024 01:32:04 +0900 Subject: [PATCH 08/14] test: update preload sourcemap snapshot --- playground/js-sourcemap/__tests__/js-sourcemap.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playground/js-sourcemap/__tests__/js-sourcemap.spec.ts b/playground/js-sourcemap/__tests__/js-sourcemap.spec.ts index be37165a35c10e..42cd698b873dc2 100644 --- a/playground/js-sourcemap/__tests__/js-sourcemap.spec.ts +++ b/playground/js-sourcemap/__tests__/js-sourcemap.spec.ts @@ -137,7 +137,7 @@ describe.runIf(isBuild)('build tests', () => { const map = findAssetFile(/after-preload-dynamic.*\.js\.map/) expect(formatSourcemapForSnapshot(JSON.parse(map))).toMatchInlineSnapshot(` { - "mappings": ";;;;;;i3BAAA,OAAO,2BAAuB,EAAC,wBAE/B,QAAQ,IAAI,uBAAuB", + "mappings": ";;;;;;08BAAA,OAAO,2BAAuB,EAAC,wBAE/B,QAAQ,IAAI,uBAAuB", "sources": [ "../../after-preload-dynamic.js", ], From 020c97190d21c9276b82bd5b05c03023519ac44f Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Thu, 29 Feb 2024 16:49:15 +0900 Subject: [PATCH 09/14] feat: don't add nonce attribute to style tags --- packages/vite/src/node/plugins/html.ts | 3 +-- playground/csp/index.html | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/vite/src/node/plugins/html.ts b/packages/vite/src/node/plugins/html.ts index 087ca95675890f..5d309f8cd7e75e 100644 --- a/packages/vite/src/node/plugins/html.ts +++ b/packages/vite/src/node/plugins/html.ts @@ -1173,8 +1173,7 @@ export function injectNonceAttributeTagHook( (attr) => attr.name === 'rel' && parseRelAttr(attr.value).some((a) => processRelType.has(a)), - )) || - (node.nodeName === 'style' && node.childNodes.length) + )) ) { s.appendRight( node.sourceCodeLocation!.startTag!.endOffset - 1, diff --git a/playground/csp/index.html b/playground/csp/index.html index 7196d0a436bc8b..e782bad46e1b8c 100644 --- a/playground/csp/index.html +++ b/playground/csp/index.html @@ -1,5 +1,5 @@ -