diff --git a/.changeset/all-pandas-appear.md b/.changeset/all-pandas-appear.md new file mode 100644 index 0000000000..0025cc1836 --- /dev/null +++ b/.changeset/all-pandas-appear.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: check to make sure `svelte:element` tags are valid during SSR diff --git a/documentation/docs/98-reference/.generated/server-errors.md b/documentation/docs/98-reference/.generated/server-errors.md index c98756afec..0ee6fd0614 100644 --- a/documentation/docs/98-reference/.generated/server-errors.md +++ b/documentation/docs/98-reference/.generated/server-errors.md @@ -16,6 +16,14 @@ Encountered asynchronous work while rendering synchronously. You (or the framework you're using) called [`render(...)`](svelte-server#render) with a component containing an `await` expression. Either `await` the result of `render` or wrap the `await` (or the component containing it) in a [``](svelte-boundary) with a `pending` snippet. +### dynamic_element_invalid_tag + +``` +`` is not a valid element name — the element will not be rendered +``` + +The value passed to the `this` prop of `` must be a valid HTML element, SVG element, MathML element, or custom element name. A value containing invalid characters (such as whitespace or special characters) was provided, which could be a security risk. Ensure only valid tag names are passed. + ### html_deprecated ``` diff --git a/packages/svelte/messages/server-errors/errors.md b/packages/svelte/messages/server-errors/errors.md index fd4c17e2a7..533e5405ed 100644 --- a/packages/svelte/messages/server-errors/errors.md +++ b/packages/svelte/messages/server-errors/errors.md @@ -10,6 +10,12 @@ Some platforms require configuration flags to enable this API. Consult your plat You (or the framework you're using) called [`render(...)`](svelte-server#render) with a component containing an `await` expression. Either `await` the result of `render` or wrap the `await` (or the component containing it) in a [``](svelte-boundary) with a `pending` snippet. +## dynamic_element_invalid_tag + +> `` is not a valid element name — the element will not be rendered + +The value passed to the `this` prop of `` must be a valid HTML element, SVG element, MathML element, or custom element name. A value containing invalid characters (such as whitespace or special characters) was provided, which could be a security risk. Ensure only valid tag names are passed. + ## html_deprecated > The `html` property of server render results has been deprecated. Use `body` instead. diff --git a/packages/svelte/src/compiler/phases/1-parse/state/element.js b/packages/svelte/src/compiler/phases/1-parse/state/element.js index d9fe33bbac..91d072c1f3 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/element.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/element.js @@ -2,7 +2,7 @@ /** @import { Location } from 'locate-character' */ /** @import { AST } from '#compiler' */ /** @import { Parser } from '../index.js' */ -import { is_void } from '../../../../utils.js'; +import { is_void, REGEX_VALID_TAG_NAME } from '../../../../utils.js'; import read_expression from '../read/expression.js'; import { read_script } from '../read/script.js'; import read_style from '../read/style.js'; @@ -24,8 +24,15 @@ const regex_whitespace_or_slash_or_closing_tag = /(\s|\/|>)/; const regex_token_ending_character = /[\s=/>"']/; const regex_starts_with_quote_characters = /^["']/; const regex_attribute_value = /^(?:"([^"]*)"|'([^'])*'|([^>\s]+))/; -const regex_valid_element_name = - /^(?:![a-zA-Z]+|[a-zA-Z](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?|[a-zA-Z][a-zA-Z0-9]*:[a-zA-Z][a-zA-Z0-9-]*[a-zA-Z0-9])$/; +/** @param {string} name */ +function is_valid_element_name(name) { + // DOCTYPE (e.g. !DOCTYPE) + if (/^![a-zA-Z]+$/.test(name)) return true; + // svelte:* meta tags (e.g. svelte:element, svelte:head) + if (/^[a-zA-Z][a-zA-Z0-9]*:[a-zA-Z][a-zA-Z0-9-]*[a-zA-Z0-9]$/.test(name)) return true; + // standard HTML/SVG/MathML elements and custom elements + return REGEX_VALID_TAG_NAME.test(name); +} export const regex_valid_component_name = // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#identifiers adjusted for our needs // (must start with uppercase letter if no dots, can contain dots) @@ -134,7 +141,7 @@ export default function element(parser) { e.svelte_meta_invalid_tag(bounds, list(Array.from(meta_tags.keys()))); } - if (!regex_valid_element_name.test(tag.name) && !regex_valid_component_name.test(tag.name)) { + if (!is_valid_element_name(tag.name) && !regex_valid_component_name.test(tag.name)) { // in the middle of typing -> allow in loose mode if (!parser.loose || !tag.name.endsWith('.')) { const bounds = { start: start + 1, end: start + 1 + tag.name.length }; diff --git a/packages/svelte/src/internal/server/errors.js b/packages/svelte/src/internal/server/errors.js index c966c32062..15f8b2174d 100644 --- a/packages/svelte/src/internal/server/errors.js +++ b/packages/svelte/src/internal/server/errors.js @@ -14,6 +14,19 @@ export function async_local_storage_unavailable() { throw error; } +/** + * `` is not a valid element name — the element will not be rendered + * @param {string} tag + * @returns {never} + */ +export function dynamic_element_invalid_tag(tag) { + const error = new Error(`dynamic_element_invalid_tag\n\`\` is not a valid element name — the element will not be rendered\nhttps://svelte.dev/e/dynamic_element_invalid_tag`); + + error.name = 'Svelte error'; + + throw error; +} + /** * Encountered asynchronous work while rendering synchronously. * @returns {never} diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index d978ed6355..06023be494 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -13,9 +13,14 @@ import { } from '../../constants.js'; import { escape_html } from '../../escaping.js'; import { DEV } from 'esm-env'; -import { EMPTY_COMMENT, BLOCK_CLOSE, BLOCK_OPEN, BLOCK_OPEN_ELSE } from './hydration.js'; +import { EMPTY_COMMENT, BLOCK_OPEN, BLOCK_OPEN_ELSE } from './hydration.js'; import { validate_store } from '../shared/validate.js'; -import { is_boolean_attribute, is_raw_text_element, is_void } from '../../utils.js'; +import { + is_boolean_attribute, + is_raw_text_element, + is_void, + REGEX_VALID_TAG_NAME +} from '../../utils.js'; import { Renderer } from './renderer.js'; import * as e from './errors.js'; @@ -35,6 +40,9 @@ export function element(renderer, tag, attributes_fn = noop, children_fn = noop) renderer.push(''); if (tag) { + if (!REGEX_VALID_TAG_NAME.test(tag)) { + e.dynamic_element_invalid_tag(tag); + } renderer.push(`<${tag}`); attributes_fn(); renderer.push(`>`); diff --git a/packages/svelte/src/utils.js b/packages/svelte/src/utils.js index d63d4ff801..57561e6dc7 100644 --- a/packages/svelte/src/utils.js +++ b/packages/svelte/src/utils.js @@ -480,6 +480,19 @@ export function is_raw_text_element(name) { return RAW_TEXT_ELEMENTS.includes(/** @type {typeof RAW_TEXT_ELEMENTS[number]} */ (name)); } +// Matches valid HTML/SVG/MathML element names and custom element names. +// https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name +// +// Standard elements: ASCII alpha start, followed by ASCII alphanumerics. +// Custom elements: ASCII alpha start, followed by any mix of PCENChar (which +// includes ASCII alphanumerics, `-`, `.`, `_`, and specified Unicode ranges), +// with at least one hyphen required somewhere after the first character. +// +// Rejects strings containing whitespace, quotes, angle brackets, slashes, equals, +// or other characters that could break out of a tag-name token and enable markup injection. +export const REGEX_VALID_TAG_NAME = + /^[a-zA-Z][a-zA-Z0-9]*(-[a-zA-Z0-9.\-_\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\u{10000}-\u{EFFFF}]+)*$/u; + /** * Prevent devtools trying to make `location` a clickable link by inserting a zero-width space * @template {string | undefined} T diff --git a/packages/svelte/tests/server-side-rendering/samples/dynamic-element-xss-prevention/_config.js b/packages/svelte/tests/server-side-rendering/samples/dynamic-element-xss-prevention/_config.js new file mode 100644 index 0000000000..ddbe9a4b16 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/dynamic-element-xss-prevention/_config.js @@ -0,0 +1,8 @@ +import { test } from '../../test'; + +export default test({ + props: { + tag: 'svg onload=alert(1)' + }, + error: 'dynamic_element_invalid_tag' +}); diff --git a/packages/svelte/tests/server-side-rendering/samples/dynamic-element-xss-prevention/main.svelte b/packages/svelte/tests/server-side-rendering/samples/dynamic-element-xss-prevention/main.svelte new file mode 100644 index 0000000000..94ad88cf0a --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/dynamic-element-xss-prevention/main.svelte @@ -0,0 +1,5 @@ + + +ok