Skip to content

Commit 83e96e0

Browse files
authored
fix: escape < in attribute strings (#12989)
Svelte 4 version of #11411
1 parent 5ec4409 commit 83e96e0

File tree

13 files changed

+59
-38
lines changed

13 files changed

+59
-38
lines changed

.changeset/itchy-ties-argue.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: escape `<` in attribute strings

.github/workflows/ci.yml

+8-8
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,19 @@ jobs:
1717
strategy:
1818
matrix:
1919
include:
20-
- node-version: 16
20+
- node-version: 18
2121
os: ubuntu-latest
22-
- node-version: 16
22+
- node-version: 18
2323
os: windows-latest
24-
- node-version: 16
25-
os: macOS-latest
2624
- node-version: 18
27-
os: ubuntu-latest
25+
os: macOS-latest
2826
- node-version: 20
2927
os: ubuntu-latest
28+
- node-version: 22
29+
os: ubuntu-latest
3030
steps:
3131
- uses: actions/checkout@v3
32-
- uses: pnpm/action-setup@v2.2.4
32+
- uses: pnpm/action-setup@v4
3333
- uses: actions/setup-node@v3
3434
with:
3535
node-version: ${{ matrix.node-version }}
@@ -44,10 +44,10 @@ jobs:
4444
timeout-minutes: 5
4545
steps:
4646
- uses: actions/checkout@v3
47-
- uses: pnpm/action-setup@v2.2.4
47+
- uses: pnpm/action-setup@v4
4848
- uses: actions/setup-node@v3
4949
with:
50-
node-version: 16
50+
node-version: 18
5151
cache: pnpm
5252
- name: install
5353
run: pnpm install --frozen-lockfile

.github/workflows/release.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ jobs:
2121
with:
2222
# This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits
2323
fetch-depth: 0
24-
- uses: pnpm/action-setup@v2.2.4
24+
- uses: pnpm/action-setup@v4
2525
- name: Setup Node.js
2626
uses: actions/setup-node@v3
2727
with:

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,5 @@
3030
"prettier": "^2.8.8",
3131
"prettier-plugin-svelte": "^2.10.1"
3232
},
33-
"packageManager": "pnpm@8.6.3"
33+
"packageManager": "pnpm@9.4.0"
3434
}

packages/svelte/src/compiler/compile/render_ssr/handlers/shared/get_attribute_value.js

+2-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { string_literal } from '../../../utils/stringify.js';
22
import { x } from 'code-red';
3-
import { regex_double_quotes } from '../../../../utils/patterns.js';
3+
import { escape } from '../../../../../shared/utils/escape.js';
44

55
/**
66
* @param {import('../../../nodes/Attribute.js').default} attribute
@@ -37,9 +37,7 @@ export function get_attribute_value(attribute) {
3737
return attribute.chunks
3838
.map((chunk) => {
3939
return chunk.type === 'Text'
40-
? /** @type {import('estree').Expression} */ (
41-
string_literal(chunk.data.replace(regex_double_quotes, '&quot;'))
42-
)
40+
? /** @type {import('estree').Expression} */ (string_literal(escape(chunk.data, true)))
4341
: x`@escape(${chunk.node}, ${is_textarea_value ? 'false' : 'true'})`;
4442
})
4543
.reduce((lhs, rhs) => x`${lhs} + ${rhs}`);

packages/svelte/src/runtime/internal/ssr.js

+2-24
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import { set_current_component, current_component } from './lifecycle.js';
22
import { run_all, blank_object } from './utils.js';
33
import { boolean_attributes } from '../../shared/boolean_attributes.js';
44
import { ensure_array_like } from './each.js';
5+
import { escape } from '../../shared/utils/escape.js';
56
export { is_void } from '../../shared/utils/names.js';
7+
export { escape };
68

79
export const invalid_attribute_name_character =
810
/[\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) {
6769
return style_object;
6870
}
6971

70-
const ATTR_REGEX = /[&"]/g;
71-
const CONTENT_REGEX = /[&<]/g;
72-
73-
/**
74-
* Note: this method is performance sensitive and has been optimized
75-
* https://github.com/sveltejs/svelte/pull/5701
76-
* @param {unknown} value
77-
* @returns {string}
78-
*/
79-
export function escape(value, is_attr = false) {
80-
const str = String(value);
81-
const pattern = is_attr ? ATTR_REGEX : CONTENT_REGEX;
82-
pattern.lastIndex = 0;
83-
let escaped = '';
84-
let last = 0;
85-
while (pattern.test(str)) {
86-
const i = pattern.lastIndex - 1;
87-
const ch = str[i];
88-
escaped += str.substring(last, i) + (ch === '&' ? '&amp;' : ch === '"' ? '&quot;' : '&lt;');
89-
last = i + 1;
90-
}
91-
return escaped + str.substring(last);
92-
}
93-
9472
export function escape_attribute_value(value) {
9573
// keep booleans, null, and undefined for the sake of `spread`
9674
const should_escape = typeof value === 'string' || (value && typeof value === 'object');
+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
const ATTR_REGEX = /[&"<]/g;
2+
const CONTENT_REGEX = /[&<]/g;
3+
4+
/**
5+
* Note: this method is performance sensitive and has been optimized
6+
* https://github.com/sveltejs/svelte/pull/5701
7+
* @param {unknown} value
8+
* @returns {string}
9+
*/
10+
export function escape(value, is_attr = false) {
11+
const str = String(value);
12+
const pattern = is_attr ? ATTR_REGEX : CONTENT_REGEX;
13+
pattern.lastIndex = 0;
14+
let escaped = '';
15+
let last = 0;
16+
while (pattern.test(str)) {
17+
const i = pattern.lastIndex - 1;
18+
const ch = str[i];
19+
escaped += str.substring(last, i) + (ch === '&' ? '&amp;' : ch === '"' ? '&quot;' : '&lt;');
20+
last = i + 1;
21+
}
22+
return escaped + str.substring(last);
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<noscript
2+
><a href="&lt;/noscript>&lt;script>console.log('should not run')&lt;/script>">test</a></noscript
3+
>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<script>
2+
const x = `</noscript><script>console.log('should not run')<` + `/script>`
3+
</script>
4+
5+
<noscript>
6+
<a href={x}>test</a>
7+
</noscript>
8+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<div title="&amp;&lt;">blah</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<div title="&amp;&lt;">blah</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<noscript><a href="&lt;/noscript>&lt;script>throw new Error('fooo')&lt;/script>">test</a></noscript>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<noscript>
2+
<a href="</noscript><script>throw new Error('fooo')</script>">test</a>
3+
</noscript>

0 commit comments

Comments
 (0)