Potential mXSS vulnerability due to improper HTML escaping in svelte
Description
svelte performance oriented web framework. A potential mXSS vulnerability exists in Svelte for versions up to but not including 4.2.19. Svelte improperly escapes HTML on server-side rendering. The assumption is that attributes will always stay as such, but in some situation the final DOM tree rendered on browsers is different from what Svelte expects on server-side rendering. This may be leveraged to perform XSS attacks, and a type of the XSS is known as mXSS (mutation XSS). More specifically, this can occur when injecting malicious content into an attribute within a noscript tag. This issue has been addressed in release version 4.2.19. Users are advised to upgrade. There are no known workarounds for this vulnerability.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
sveltenpm | < 4.2.19 | 4.2.19 |
Affected products
1Patches
183e96e044debfix: escape `<` in attribute strings (#12989)
13 files changed · +59 −38
.changeset/itchy-ties-argue.md+5 −0 added@@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: escape `<` in attribute strings
.github/workflows/ci.yml+8 −8 modified@@ -17,19 +17,19 @@ jobs: strategy: matrix: include: - - node-version: 16 + - node-version: 18 os: ubuntu-latest - - node-version: 16 + - node-version: 18 os: windows-latest - - node-version: 16 - os: macOS-latest - node-version: 18 - os: ubuntu-latest + os: macOS-latest - node-version: 20 os: ubuntu-latest + - node-version: 22 + os: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: pnpm/action-setup@v2.2.4 + - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} @@ -44,10 +44,10 @@ jobs: timeout-minutes: 5 steps: - uses: actions/checkout@v3 - - uses: pnpm/action-setup@v2.2.4 + - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v3 with: - node-version: 16 + node-version: 18 cache: pnpm - name: install run: pnpm install --frozen-lockfile
.github/workflows/release.yml+1 −1 modified@@ -21,7 +21,7 @@ jobs: with: # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits fetch-depth: 0 - - uses: pnpm/action-setup@v2.2.4 + - uses: pnpm/action-setup@v4 - name: Setup Node.js uses: actions/setup-node@v3 with:
package.json+1 −1 modified@@ -30,5 +30,5 @@ "prettier": "^2.8.8", "prettier-plugin-svelte": "^2.10.1" }, - "packageManager": "pnpm@8.6.3" + "packageManager": "pnpm@9.4.0" }
packages/svelte/src/compiler/compile/render_ssr/handlers/shared/get_attribute_value.js+2 −4 modified@@ -1,6 +1,6 @@ import { string_literal } from '../../../utils/stringify.js'; import { x } from 'code-red'; -import { regex_double_quotes } from '../../../../utils/patterns.js'; +import { escape } from '../../../../../shared/utils/escape.js'; /** * @param {import('../../../nodes/Attribute.js').default} attribute @@ -37,9 +37,7 @@ export function get_attribute_value(attribute) { return attribute.chunks .map((chunk) => { return chunk.type === 'Text' - ? /** @type {import('estree').Expression} */ ( - string_literal(chunk.data.replace(regex_double_quotes, '"')) - ) + ? /** @type {import('estree').Expression} */ (string_literal(escape(chunk.data, true))) : x`@escape(${chunk.node}, ${is_textarea_value ? 'false' : 'true'})`; }) .reduce((lhs, rhs) => x`${lhs} + ${rhs}`);
packages/svelte/src/runtime/internal/ssr.js+2 −24 modified@@ -2,7 +2,9 @@ import { set_current_component, current_component } from './lifecycle.js'; import { run_all, blank_object } from './utils.js'; import { boolean_attributes } from '../../shared/boolean_attributes.js'; import { ensure_array_like } from './each.js'; +import { escape } from '../../shared/utils/escape.js'; export { is_void } from '../../shared/utils/names.js'; +export { escape }; export const invalid_attribute_name_character = /[\s'">/=\u{FDD0}-\u{FDEF}\u{FFFE}\u{FFFF}\u{1FFFE}\u{1FFFF}\u{2FFFE}\u{2FFFF}\u{3FFFE}\u{3FFFF}\u{4FFFE}\u{4FFFF}\u{5FFFE}\u{5FFFF}\u{6FFFE}\u{6FFFF}\u{7FFFE}\u{7FFFF}\u{8FFFE}\u{8FFFF}\u{9FFFE}\u{9FFFF}\u{AFFFE}\u{AFFFF}\u{BFFFE}\u{BFFFF}\u{CFFFE}\u{CFFFF}\u{DFFFE}\u{DFFFF}\u{EFFFE}\u{EFFFF}\u{FFFFE}\u{FFFFF}\u{10FFFE}\u{10FFFF}]/u; @@ -67,30 +69,6 @@ export function merge_ssr_styles(style_attribute, style_directive) { return style_object; } -const ATTR_REGEX = /[&"]/g; -const CONTENT_REGEX = /[&<]/g; - -/** - * Note: this method is performance sensitive and has been optimized - * https://github.com/sveltejs/svelte/pull/5701 - * @param {unknown} value - * @returns {string} - */ -export function escape(value, is_attr = false) { - const str = String(value); - const pattern = is_attr ? ATTR_REGEX : CONTENT_REGEX; - pattern.lastIndex = 0; - let escaped = ''; - let last = 0; - while (pattern.test(str)) { - const i = pattern.lastIndex - 1; - const ch = str[i]; - escaped += str.substring(last, i) + (ch === '&' ? '&' : ch === '"' ? '"' : '<'); - last = i + 1; - } - return escaped + str.substring(last); -} - export function escape_attribute_value(value) { // keep booleans, null, and undefined for the sake of `spread` const should_escape = typeof value === 'string' || (value && typeof value === 'object');
packages/svelte/src/shared/utils/escape.js+23 −0 added@@ -0,0 +1,23 @@ +const ATTR_REGEX = /[&"<]/g; +const CONTENT_REGEX = /[&<]/g; + +/** + * Note: this method is performance sensitive and has been optimized + * https://github.com/sveltejs/svelte/pull/5701 + * @param {unknown} value + * @returns {string} + */ +export function escape(value, is_attr = false) { + const str = String(value); + const pattern = is_attr ? ATTR_REGEX : CONTENT_REGEX; + pattern.lastIndex = 0; + let escaped = ''; + let last = 0; + while (pattern.test(str)) { + const i = pattern.lastIndex - 1; + const ch = str[i]; + escaped += str.substring(last, i) + (ch === '&' ? '&' : ch === '"' ? '"' : '<'); + last = i + 1; + } + return escaped + str.substring(last); +}
packages/svelte/test/server-side-rendering/samples/escaped-attr-2/_expected.html+3 −0 added@@ -0,0 +1,3 @@ +<noscript + ><a href="</noscript><script>console.log('should not run')</script>">test</a></noscript +>
packages/svelte/test/server-side-rendering/samples/escaped-attr-2/main.svelte+8 −0 added@@ -0,0 +1,8 @@ +<script> + const x = `</noscript><script>console.log('should not run')<` + `/script>` +</script> + +<noscript> + <a href={x}>test</a> +</noscript> +
packages/svelte/test/server-side-rendering/samples/escaped-attr-3/_expected.html+1 −0 added@@ -0,0 +1 @@ +<div title="&<">blah</div>
packages/svelte/test/server-side-rendering/samples/escaped-attr-3/main.svelte+1 −0 added@@ -0,0 +1 @@ +<div title="&<">blah</div>
packages/svelte/test/server-side-rendering/samples/escaped-attr/_expected.html+1 −0 added@@ -0,0 +1 @@ +<noscript><a href="</noscript><script>throw new Error('fooo')</script>">test</a></noscript>
packages/svelte/test/server-side-rendering/samples/escaped-attr/main.svelte+3 −0 added@@ -0,0 +1,3 @@ +<noscript> + <a href="</noscript><script>throw new Error('fooo')</script>">test</a> +</noscript>
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4- github.com/advisories/GHSA-8266-84wp-wv5cghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-45047ghsaADVISORY
- github.com/sveltejs/svelte/commit/83e96e044deb5ecbae2af361ae9e31d3e1ac43a3ghsaWEB
- github.com/sveltejs/svelte/security/advisories/GHSA-8266-84wp-wv5cghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.