From 507da0b3fa2fa8c37657438e4f99fc7b2d481a7d Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 23 Aug 2024 17:12:08 +0200 Subject: [PATCH 1/5] fix: escape `<` in attribute strings Svelte 4 version of #11411 --- .changeset/itchy-ties-argue.md | 5 ++++ .../handlers/shared/get_attribute_value.js | 6 ++--- packages/svelte/src/runtime/internal/ssr.js | 26 ++----------------- packages/svelte/src/shared/utils/escape.js | 23 ++++++++++++++++ .../samples/escaped-attr-2/_expected.html | 3 +++ .../samples/escaped-attr-2/main.svelte | 8 ++++++ .../samples/escaped-attr-3/_expected.html | 1 + .../samples/escaped-attr-3/main.svelte | 1 + .../samples/escaped-attr/_expected.html | 1 + .../samples/escaped-attr/main.svelte | 3 +++ 10 files changed, 49 insertions(+), 28 deletions(-) create mode 100644 .changeset/itchy-ties-argue.md create mode 100644 packages/svelte/src/shared/utils/escape.js create mode 100644 packages/svelte/test/server-side-rendering/samples/escaped-attr-2/_expected.html create mode 100644 packages/svelte/test/server-side-rendering/samples/escaped-attr-2/main.svelte create mode 100644 packages/svelte/test/server-side-rendering/samples/escaped-attr-3/_expected.html create mode 100644 packages/svelte/test/server-side-rendering/samples/escaped-attr-3/main.svelte create mode 100644 packages/svelte/test/server-side-rendering/samples/escaped-attr/_expected.html create mode 100644 packages/svelte/test/server-side-rendering/samples/escaped-attr/main.svelte diff --git a/.changeset/itchy-ties-argue.md b/.changeset/itchy-ties-argue.md new file mode 100644 index 000000000000..e5936585fa77 --- /dev/null +++ b/.changeset/itchy-ties-argue.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: escape `<` in attribute strings 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 fb3136cc858a..d88c16842222 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 1917f0cfbc69..b92e4ed2a4ac 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 000000000000..078d5ca715e3 --- /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 000000000000..3f2ff23dc2b0 --- /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 000000000000..ec1ecfcac58d --- /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 000000000000..ff33fbf59a73 --- /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 000000000000..ff33fbf59a73 --- /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 000000000000..a69ed31ade51 --- /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 000000000000..8cc01b958f17 --- /dev/null +++ b/packages/svelte/test/server-side-rendering/samples/escaped-attr/main.svelte @@ -0,0 +1,3 @@ + From b19344ad45ef7ac04172d32aa7a2366661ee55e8 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 23 Aug 2024 17:23:39 +0200 Subject: [PATCH 2/5] fix CI --- .github/workflows/ci.yml | 4 ++-- .github/workflows/release.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c04ee83f97ab..7c996d233d4f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: os: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: pnpm/action-setup@v2.2.4 + - uses: pnpm/action-setup@v2.4.1 - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} @@ -44,7 +44,7 @@ jobs: timeout-minutes: 5 steps: - uses: actions/checkout@v3 - - uses: pnpm/action-setup@v2.2.4 + - uses: pnpm/action-setup@v2.4.1 - uses: actions/setup-node@v3 with: node-version: 16 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 06b94803bb7b..cc7769b473e3 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@v2.4.1 - name: Setup Node.js uses: actions/setup-node@v3 with: From 7944b823dceddc288c2f9ed990dfda8f050151c9 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 23 Aug 2024 17:28:39 +0200 Subject: [PATCH 3/5] fml --- .github/workflows/ci.yml | 4 ++-- .github/workflows/release.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7c996d233d4f..b587f42fc01d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: os: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: pnpm/action-setup@v2.4.1 + - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} @@ -44,7 +44,7 @@ jobs: timeout-minutes: 5 steps: - uses: actions/checkout@v3 - - uses: pnpm/action-setup@v2.4.1 + - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v3 with: node-version: 16 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cc7769b473e3..fe5ce6fc7e44 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.4.1 + - uses: pnpm/action-setup@v4 - name: Setup Node.js uses: actions/setup-node@v3 with: From f6f95e97ee0870b20a47920ac63183e4c54d8b61 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 23 Aug 2024 17:32:32 +0200 Subject: [PATCH 4/5] try this --- .github/workflows/ci.yml | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b587f42fc01d..3192ae31b32d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: os: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v2.4.1 - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} @@ -47,7 +47,7 @@ jobs: - 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/package.json b/package.json index 4d22d9642a55..29a2ab047bc4 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" } From fb22bd9bb9a1ec9c7500fdca6c1c3284b44a2e7d Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 23 Aug 2024 17:36:44 +0200 Subject: [PATCH 5/5] remove node 16 then, we won't break compatibility anyway because we're not using any node APIs --- .github/workflows/ci.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3192ae31b32d..b90628c6e2d2 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.4.1 + - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }}