fix: escape `<` in attribute strings (#12989)

Svelte 4 version of #11411
pull/12990/head
Simon H 3 months ago committed by GitHub
parent 5ec4409fb2
commit 83e96e044d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: escape `<` in attribute strings

@ -17,19 +17,19 @@ jobs:
strategy: strategy:
matrix: matrix:
include: include:
- node-version: 16 - node-version: 18
os: ubuntu-latest os: ubuntu-latest
- node-version: 16 - node-version: 18
os: windows-latest os: windows-latest
- node-version: 16
os: macOS-latest
- node-version: 18 - node-version: 18
os: ubuntu-latest os: macOS-latest
- node-version: 20 - node-version: 20
os: ubuntu-latest os: ubuntu-latest
- node-version: 22
os: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: pnpm/action-setup@v2.2.4 - uses: pnpm/action-setup@v4
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
@ -44,10 +44,10 @@ jobs:
timeout-minutes: 5 timeout-minutes: 5
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: pnpm/action-setup@v2.2.4 - uses: pnpm/action-setup@v4
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
node-version: 16 node-version: 18
cache: pnpm cache: pnpm
- name: install - name: install
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile

@ -21,7 +21,7 @@ jobs:
with: with:
# This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits
fetch-depth: 0 fetch-depth: 0
- uses: pnpm/action-setup@v2.2.4 - uses: pnpm/action-setup@v4
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:

@ -30,5 +30,5 @@
"prettier": "^2.8.8", "prettier": "^2.8.8",
"prettier-plugin-svelte": "^2.10.1" "prettier-plugin-svelte": "^2.10.1"
}, },
"packageManager": "pnpm@8.6.3" "packageManager": "pnpm@9.4.0"
} }

@ -1,6 +1,6 @@
import { string_literal } from '../../../utils/stringify.js'; import { string_literal } from '../../../utils/stringify.js';
import { x } from 'code-red'; 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 * @param {import('../../../nodes/Attribute.js').default} attribute
@ -37,9 +37,7 @@ export function get_attribute_value(attribute) {
return attribute.chunks return attribute.chunks
.map((chunk) => { .map((chunk) => {
return chunk.type === 'Text' return chunk.type === 'Text'
? /** @type {import('estree').Expression} */ ( ? /** @type {import('estree').Expression} */ (string_literal(escape(chunk.data, true)))
string_literal(chunk.data.replace(regex_double_quotes, '&quot;'))
)
: x`@escape(${chunk.node}, ${is_textarea_value ? 'false' : 'true'})`; : x`@escape(${chunk.node}, ${is_textarea_value ? 'false' : 'true'})`;
}) })
.reduce((lhs, rhs) => x`${lhs} + ${rhs}`); .reduce((lhs, rhs) => x`${lhs} + ${rhs}`);

@ -2,7 +2,9 @@ import { set_current_component, current_component } from './lifecycle.js';
import { run_all, blank_object } from './utils.js'; import { run_all, blank_object } from './utils.js';
import { boolean_attributes } from '../../shared/boolean_attributes.js'; import { boolean_attributes } from '../../shared/boolean_attributes.js';
import { ensure_array_like } from './each.js'; import { ensure_array_like } from './each.js';
import { escape } from '../../shared/utils/escape.js';
export { is_void } from '../../shared/utils/names.js'; export { is_void } from '../../shared/utils/names.js';
export { escape };
export const invalid_attribute_name_character = 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; /[\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; 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 === '&' ? '&amp;' : ch === '"' ? '&quot;' : '&lt;');
last = i + 1;
}
return escaped + str.substring(last);
}
export function escape_attribute_value(value) { export function escape_attribute_value(value) {
// keep booleans, null, and undefined for the sake of `spread` // keep booleans, null, and undefined for the sake of `spread`
const should_escape = typeof value === 'string' || (value && typeof value === 'object'); const should_escape = typeof value === 'string' || (value && typeof value === 'object');

@ -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 === '&' ? '&amp;' : ch === '"' ? '&quot;' : '&lt;');
last = i + 1;
}
return escaped + str.substring(last);
}

@ -0,0 +1,3 @@
<noscript
><a href="&lt;/noscript>&lt;script>console.log('should not run')&lt;/script>">test</a></noscript
>

@ -0,0 +1,8 @@
<script>
const x = `</noscript><script>console.log('should not run')<` + `/script>`
</script>
<noscript>
<a href={x}>test</a>
</noscript>

@ -0,0 +1 @@
<noscript><a href="&lt;/noscript>&lt;script>throw new Error('fooo')&lt;/script>">test</a></noscript>

@ -0,0 +1,3 @@
<noscript>
<a href="</noscript><script>throw new Error('fooo')</script>">test</a>
</noscript>
Loading…
Cancel
Save