diff --git a/docs/guide/api-plugin.md b/docs/guide/api-plugin.md index b8ef5fb2cc697e..8f99b2c4968636 100644 --- a/docs/guide/api-plugin.md +++ b/docs/guide/api-plugin.md @@ -388,6 +388,9 @@ Vite plugins can also provide hooks that serve Vite-specific purposes. These hoo interface HtmlTagDescriptor { tag: string + /** + * attribute values will be escaped automatically if needed + */ attrs?: Record children?: string | HtmlTagDescriptor[] /** diff --git a/packages/vite/src/node/plugins/html.ts b/packages/vite/src/node/plugins/html.ts index 537f58470a9cbd..1591f50233790c 100644 --- a/packages/vite/src/node/plugins/html.ts +++ b/packages/vite/src/node/plugins/html.ts @@ -10,7 +10,6 @@ import MagicString from 'magic-string' import colors from 'picocolors' import type { DefaultTreeAdapterMap, ParserError, Token } from 'parse5' import { stripLiteral } from 'strip-literal' -import escapeHtml from 'escape-html' import type { Plugin } from '../plugin' import type { ViteDevServer } from '../server' import { @@ -1057,6 +1056,9 @@ export function extractImportExpressionFromClassicScript( export interface HtmlTagDescriptor { tag: string + /** + * attribute values will be escaped automatically if needed + */ attrs?: Record children?: string | HtmlTagDescriptor[] /** @@ -1271,7 +1273,10 @@ export function injectNonceAttributeTagHook( // is appended prior to the `/` const appendOffset = html[startTagEndOffset - 2] === '/' ? 2 : 1 - s.appendRight(startTagEndOffset - appendOffset, ` nonce="${nonce}"`) + s.appendRight( + startTagEndOffset - appendOffset, + ` nonce="${escapeForHtmlDoubleQuoteAttr(nonce)}"`, + ) } }) @@ -1571,12 +1576,21 @@ function serializeAttrs(attrs: HtmlTagDescriptor['attrs']): string { if (typeof attrs[key] === 'boolean') { res += attrs[key] ? ` ${key}` : `` } else { - res += ` ${key}="${escapeHtml(attrs[key])}"` + res += ` ${key}="${escapeForHtmlDoubleQuoteAttr(attrs[key]!)}"` } } return res } +/** + * Escape `"` / `&` which is the minimal set of characters needed to be escaped in double-quoted attributes. + * + * This is to keep `<` / `>` used by some template engines (e.g. EJS, eRuby, Mako) as interpolation syntax as-is. + */ +function escapeForHtmlDoubleQuoteAttr(s: string) { + return s.replaceAll('&', '&').replaceAll('"', '"') +} + function incrementIndent(indent: string = '') { return `${indent}${indent[0] === '\t' ? '\t' : ' '}` } diff --git a/playground/html/__tests__/html.spec.ts b/playground/html/__tests__/html.spec.ts index 55b587f928fe30..f23d2b7739868f 100644 --- a/playground/html/__tests__/html.spec.ts +++ b/playground/html/__tests__/html.spec.ts @@ -507,6 +507,11 @@ test('html fallback works non browser accept header', async () => { }) test('escape html attribute', async () => { + const content = await fetch(viteTestUrl) + // only escape double quote and ampersand + expect(await content.text()).toContain( + `">
extra content'&
`, + ) const el = await page.$('.unescape-div') expect(el).toBeNull() }) diff --git a/playground/html/vite.config.js b/playground/html/vite.config.js index ff59f5db10a2c5..872ad04ac4e01c 100644 --- a/playground/html/vite.config.js +++ b/playground/html/vite.config.js @@ -222,7 +222,8 @@ ${ { tag: 'link', attrs: { - href: `">
extra content
`, + class: 'escape-html-attribute', + href: `">
extra content'&
`, }, injectTo: 'body', },