diff --git a/.changeset/itchy-ties-argue.md b/.changeset/itchy-ties-argue.md new file mode 100644 index 0000000000..e5936585fa --- /dev/null +++ b/.changeset/itchy-ties-argue.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: escape `<` in attribute strings diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c04ee83f97..b90628c6e2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 06b94803bb..fe5ce6fc7e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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: diff --git a/package.json b/package.json index 4d22d9642a..29a2ab047b 100644 --- a/package.json +++ b/package.json @@ -30,5 +30,5 @@ "prettier": "^2.8.8", "prettier-plugin-svelte": "^2.10.1" }, - "packageManager": "pnpm@8.6.3" + "packageManager": "pnpm@9.4.0" } diff --git a/packages/svelte/src/compiler/compile/render_ssr/handlers/shared/get_attribute_value.js b/packages/svelte/src/compiler/compile/render_ssr/handlers/shared/get_attribute_value.js index fb3136cc85..d88c168422 100644 --- a/packages/svelte/src/compiler/compile/render_ssr/handlers/shared/get_attribute_value.js +++ b/packages/svelte/src/compiler/compile/render_ssr/handlers/shared/get_attribute_value.js @@ -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}`); diff --git a/packages/svelte/src/runtime/internal/ssr.js b/packages/svelte/src/runtime/internal/ssr.js index 1917f0cfbc..b92e4ed2a4 100644 --- a/packages/svelte/src/runtime/internal/ssr.js +++ b/packages/svelte/src/runtime/internal/ssr.js @@ -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'); diff --git a/packages/svelte/src/shared/utils/escape.js b/packages/svelte/src/shared/utils/escape.js new file mode 100644 index 0000000000..078d5ca715 --- /dev/null +++ b/packages/svelte/src/shared/utils/escape.js @@ -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); +} diff --git a/packages/svelte/test/server-side-rendering/samples/escaped-attr-2/_expected.html b/packages/svelte/test/server-side-rendering/samples/escaped-attr-2/_expected.html new file mode 100644 index 0000000000..3f2ff23dc2 --- /dev/null +++ b/packages/svelte/test/server-side-rendering/samples/escaped-attr-2/_expected.html @@ -0,0 +1,3 @@ + diff --git a/packages/svelte/test/server-side-rendering/samples/escaped-attr-2/main.svelte b/packages/svelte/test/server-side-rendering/samples/escaped-attr-2/main.svelte new file mode 100644 index 0000000000..ec1ecfcac5 --- /dev/null +++ b/packages/svelte/test/server-side-rendering/samples/escaped-attr-2/main.svelte @@ -0,0 +1,8 @@ + + + + diff --git a/packages/svelte/test/server-side-rendering/samples/escaped-attr-3/_expected.html b/packages/svelte/test/server-side-rendering/samples/escaped-attr-3/_expected.html new file mode 100644 index 0000000000..ff33fbf59a --- /dev/null +++ b/packages/svelte/test/server-side-rendering/samples/escaped-attr-3/_expected.html @@ -0,0 +1 @@ +
blah
diff --git a/packages/svelte/test/server-side-rendering/samples/escaped-attr-3/main.svelte b/packages/svelte/test/server-side-rendering/samples/escaped-attr-3/main.svelte new file mode 100644 index 0000000000..ff33fbf59a --- /dev/null +++ b/packages/svelte/test/server-side-rendering/samples/escaped-attr-3/main.svelte @@ -0,0 +1 @@ +
blah
diff --git a/packages/svelte/test/server-side-rendering/samples/escaped-attr/_expected.html b/packages/svelte/test/server-side-rendering/samples/escaped-attr/_expected.html new file mode 100644 index 0000000000..a69ed31ade --- /dev/null +++ b/packages/svelte/test/server-side-rendering/samples/escaped-attr/_expected.html @@ -0,0 +1 @@ + diff --git a/packages/svelte/test/server-side-rendering/samples/escaped-attr/main.svelte b/packages/svelte/test/server-side-rendering/samples/escaped-attr/main.svelte new file mode 100644 index 0000000000..8cc01b958f --- /dev/null +++ b/packages/svelte/test/server-side-rendering/samples/escaped-attr/main.svelte @@ -0,0 +1,3 @@ +