diff --git a/packages/vite/src/node/plugins/html.ts b/packages/vite/src/node/plugins/html.ts index c056752ea6326d..8b7908a0d288bc 100644 --- a/packages/vite/src/node/plugins/html.ts +++ b/packages/vite/src/node/plugins/html.ts @@ -10,6 +10,7 @@ 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 { @@ -1510,7 +1511,7 @@ function serializeAttrs(attrs: HtmlTagDescriptor['attrs']): string { if (typeof attrs[key] === 'boolean') { res += attrs[key] ? ` ${key}` : `` } else { - res += ` ${key}=${JSON.stringify(attrs[key])}` + res += ` ${key}="${escapeHtml(attrs[key])}"` } } return res diff --git a/playground/html/__tests__/html.spec.ts b/playground/html/__tests__/html.spec.ts index b3b8582d9a14e1..1d419346a5ef0e 100644 --- a/playground/html/__tests__/html.spec.ts +++ b/playground/html/__tests__/html.spec.ts @@ -471,3 +471,8 @@ test('html fallback works non browser accept header', async () => { ).status, ).toBe(200) }) + +test('escape html attribute', async () => { + const el = await page.$('.unescape-div') + expect(el).toBeNull() +}) diff --git a/playground/html/vite.config.js b/playground/html/vite.config.js index f8c14eef4ca402..cc3f6ba3a4ef34 100644 --- a/playground/html/vite.config.js +++ b/playground/html/vite.config.js @@ -214,5 +214,22 @@ ${ ] }, }, + { + name: 'escape-html-attribute', + transformIndexHtml: { + order: 'post', + handler() { + return [ + { + tag: 'link', + attrs: { + href: `">
extra content
`, + }, + injectTo: 'body', + }, + ] + }, + }, + }, ], })