diff --git a/.changeset/big-webs-sing.md b/.changeset/big-webs-sing.md deleted file mode 100644 index 946a41d881..0000000000 --- a/.changeset/big-webs-sing.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: leave stale promises to wait for a later resolution, instead of rejecting diff --git a/.changeset/cruel-boxes-serve.md b/.changeset/cruel-boxes-serve.md deleted file mode 100644 index 592cec4d01..0000000000 --- a/.changeset/cruel-boxes-serve.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: reapply context after transforming error during SSR diff --git a/.changeset/easy-singers-retire.md b/.changeset/easy-singers-retire.md deleted file mode 100644 index 4420286e13..0000000000 --- a/.changeset/easy-singers-retire.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: don't rebase just-created batches diff --git a/.changeset/fine-bushes-marry.md b/.changeset/fine-bushes-marry.md deleted file mode 100644 index ccba53babe..0000000000 --- a/.changeset/fine-bushes-marry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -chore: allow `null` for `pending` in typings diff --git a/.changeset/flat-shrimps-worry.md b/.changeset/flat-shrimps-worry.md deleted file mode 100644 index a5f76a0f9d..0000000000 --- a/.changeset/flat-shrimps-worry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: flush eager effects in production diff --git a/.changeset/fresh-stars-grin.md b/.changeset/fresh-stars-grin.md deleted file mode 100644 index 3d56792d1e..0000000000 --- a/.changeset/fresh-stars-grin.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: rethrow error of failed iterable after calling `return()` diff --git a/.changeset/full-waves-tease.md b/.changeset/full-waves-tease.md deleted file mode 100644 index 3915334bf7..0000000000 --- a/.changeset/full-waves-tease.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: account for proxified instance when updating `bind:this` diff --git a/.changeset/many-pandas-add.md b/.changeset/many-pandas-add.md deleted file mode 100644 index 85de7acb35..0000000000 --- a/.changeset/many-pandas-add.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: ensure scheduled batch is flushed if not obsolete diff --git a/.changeset/modern-tables-fetch.md b/.changeset/modern-tables-fetch.md deleted file mode 100644 index 89543910fa..0000000000 --- a/.changeset/modern-tables-fetch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: resolve stale deriveds with latest value diff --git a/.changeset/public-mammals-float.md b/.changeset/public-mammals-float.md deleted file mode 100644 index d890c9e070..0000000000 --- a/.changeset/public-mammals-float.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -chore: remove unnecessary `increment_pending` calls diff --git a/.changeset/quiet-teams-pick.md b/.changeset/quiet-teams-pick.md deleted file mode 100644 index ed046168be..0000000000 --- a/.changeset/quiet-teams-pick.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: correctly compile component member expressions for SSR diff --git a/.changeset/red-crabs-ring.md b/.changeset/red-crabs-ring.md deleted file mode 100644 index 82b53c5464..0000000000 --- a/.changeset/red-crabs-ring.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: reset `source.updated` stack traces after `flush` diff --git a/.changeset/shiny-squids-whisper.md b/.changeset/shiny-squids-whisper.md deleted file mode 100644 index a8d2d7378c..0000000000 --- a/.changeset/shiny-squids-whisper.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"svelte": patch ---- - -fix: replacing async 'blocking' strategy with 'merging' diff --git a/.changeset/small-tools-walk.md b/.changeset/small-tools-walk.md deleted file mode 100644 index 2b275368b3..0000000000 --- a/.changeset/small-tools-walk.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: allow `@debug` tags to reference awaited variables diff --git a/.changeset/smooth-poems-tap.md b/.changeset/smooth-poems-tap.md deleted file mode 100644 index ac160656cf..0000000000 --- a/.changeset/smooth-poems-tap.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: re-run fallback props if dependencies update diff --git a/.changeset/stupid-baboons-fall.md b/.changeset/stupid-baboons-fall.md deleted file mode 100644 index 66895ad015..0000000000 --- a/.changeset/stupid-baboons-fall.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: abort running obsolete async branches diff --git a/.changeset/three-pears-build.md b/.changeset/three-pears-build.md deleted file mode 100644 index 8a638149a3..0000000000 --- a/.changeset/three-pears-build.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: ignore comments when reading CSS values diff --git a/.changeset/tough-knives-smell.md b/.changeset/tough-knives-smell.md deleted file mode 100644 index 7687188c1a..0000000000 --- a/.changeset/tough-knives-smell.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: wrap `Promise.all` in `save` during SSR diff --git a/.changeset/twelve-cooks-speak.md b/.changeset/twelve-cooks-speak.md deleted file mode 100644 index d4fcd5c339..0000000000 --- a/.changeset/twelve-cooks-speak.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: ignore false-positive errors of `$inspect` dependencies diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 8e0f8a0916..1255d8c6a0 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,63 @@ # svelte +## 5.55.7 + +### Patch Changes + +- fix: prevent XSS on `hydratable` from user contents ([`a16ebc67bbcf8f708360195687e1b2719463e1a4`](https://github.com/sveltejs/svelte/commit/a16ebc67bbcf8f708360195687e1b2719463e1a4)) + +- chore: bump devalue ([#18219](https://github.com/sveltejs/svelte/pull/18219)) + +- fix: disallow empty attribute names during SSR ([`547853e2406a2147ad7fb5ffeba95b01bd9642da`](https://github.com/sveltejs/svelte/commit/547853e2406a2147ad7fb5ffeba95b01bd9642da)) + +- fix: harden regex ([`d2375e2ebcab5c88feb5652f1a9d621b8f06b259`](https://github.com/sveltejs/svelte/commit/d2375e2ebcab5c88feb5652f1a9d621b8f06b259)) + +- fix: move Svelte runtime properties to symbols ([`e1cbbd96441e82c9eb8a23a2903c0d06d3cda991`](https://github.com/sveltejs/svelte/commit/e1cbbd96441e82c9eb8a23a2903c0d06d3cda991)) + +## 5.55.6 + +### Patch Changes + +- fix: leave stale promises to wait for a later resolution, instead of rejecting ([#18180](https://github.com/sveltejs/svelte/pull/18180)) + +- fix: keep dependencies of `$state.eager/pending` ([#18218](https://github.com/sveltejs/svelte/pull/18218)) + +- fix: reapply context after transforming error during SSR ([#18099](https://github.com/sveltejs/svelte/pull/18099)) + +- fix: don't rebase just-created batches ([#18117](https://github.com/sveltejs/svelte/pull/18117)) + +- chore: allow `null` for `pending` in typings ([#18201](https://github.com/sveltejs/svelte/pull/18201)) + +- fix: flush eager effects in production ([#18107](https://github.com/sveltejs/svelte/pull/18107)) + +- fix: rethrow error of failed iterable after calling `return()` ([#18169](https://github.com/sveltejs/svelte/pull/18169)) + +- fix: account for proxified instance when updating `bind:this` ([#18147](https://github.com/sveltejs/svelte/pull/18147)) + +- fix: ensure scheduled batch is flushed if not obsolete ([#18131](https://github.com/sveltejs/svelte/pull/18131)) + +- fix: resolve stale deriveds with latest value ([#18167](https://github.com/sveltejs/svelte/pull/18167)) + +- chore: remove unnecessary `increment_pending` calls ([#18183](https://github.com/sveltejs/svelte/pull/18183)) + +- fix: correctly compile component member expressions for SSR ([#18192](https://github.com/sveltejs/svelte/pull/18192)) + +- fix: reset `source.updated` stack traces after `flush` ([#18196](https://github.com/sveltejs/svelte/pull/18196)) + +- fix: replacing async 'blocking' strategy with 'merging' ([#18205](https://github.com/sveltejs/svelte/pull/18205)) + +- fix: allow `@debug` tags to reference awaited variables ([#18138](https://github.com/sveltejs/svelte/pull/18138)) + +- fix: re-run fallback props if dependencies update ([#18146](https://github.com/sveltejs/svelte/pull/18146)) + +- fix: abort running obsolete async branches ([#18118](https://github.com/sveltejs/svelte/pull/18118)) + +- fix: ignore comments when reading CSS values ([#18153](https://github.com/sveltejs/svelte/pull/18153)) + +- fix: wrap `Promise.all` in `save` during SSR ([#18178](https://github.com/sveltejs/svelte/pull/18178)) + +- fix: ignore false-positive errors of `$inspect` dependencies ([#18106](https://github.com/sveltejs/svelte/pull/18106)) + ## 5.55.5 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 5950c95f78..0ec2f54c4e 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.55.5", + "version": "5.55.7", "type": "module", "types": "./types/index.d.ts", "engines": { @@ -183,7 +183,7 @@ "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", - "devalue": "^5.6.4", + "devalue": "^5.8.1", "esm-env": "^1.2.1", "esrap": "^2.2.4", "is-reference": "^3.0.3", diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index c4a51db0c3..2e6369a63d 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -63,6 +63,11 @@ export const STATE_SYMBOL = Symbol('$state'); export const LEGACY_PROPS = Symbol('legacy props'); export const LOADING_ATTR_SYMBOL = Symbol(''); export const PROXY_PATH_SYMBOL = Symbol('proxy path'); +export const ATTRIBUTES_CACHE = Symbol('attributes'); +export const CLASS_CACHE = Symbol('class'); +export const STYLE_CACHE = Symbol('style'); +export const TEXT_CACHE = Symbol('text'); +export const FORM_RESET_HANDLER = Symbol('form reset'); /** An anchor might change, via this symbol on the original anchor we can tell HMR about the updated anchor */ export const HMR_ANCHOR = Symbol('hmr anchor'); diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index 5ed6e61992..7e4e30d2c0 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -5,7 +5,12 @@ import { get_descriptors, get_prototype_of } from '../../../shared/utils.js'; import { create_event, delegate, delegated, event, event_symbol } from './events.js'; import { add_form_reset_listener, autofocus } from './misc.js'; import * as w from '../../warnings.js'; -import { IS_XHTML, LOADING_ATTR_SYMBOL } from '#client/constants'; +import { + ATTRIBUTES_CACHE, + FORM_RESET_HANDLER, + IS_XHTML, + LOADING_ATTR_SYMBOL +} from '#client/constants'; import { queue_micro_task } from '../task.js'; import { is_capture_event, can_delegate_event, normalize_attribute } from '../../../../utils.js'; import { @@ -84,8 +89,7 @@ export function remove_input_defaults(input) { } }; - // @ts-expect-error - input.__on_r = remove_defaults; + /** @type {any} */ (input)[FORM_RESET_HANDLER] = remove_defaults; queue_micro_task(remove_defaults); add_form_reset_listener(); } @@ -596,8 +600,7 @@ export function attribute_effect( */ function get_attributes(element) { return /** @type {Record} **/ ( - // @ts-expect-error - element.__attributes ??= { + /** @type {any} */ (element)[ATTRIBUTES_CACHE] ??= { [IS_CUSTOM_ELEMENT]: (node_name(element) ?? '').includes('-'), [IS_HTML]: element.namespaceURI === NAMESPACE_HTML } @@ -620,13 +623,19 @@ function get_setters(element) { var proto = element; // In the case of custom elements there might be setters on the instance var element_proto = Element.prototype; - // Stop at Element, from there on there's only unnecessary setters we're not interested in + // Stop at Element, from there on there's only unnecessary (and dangerous, like innerHTML) setters we're not interested in // Do not use constructor.name here as that's unreliable in some browser environments while (element_proto !== proto) { descriptors = get_descriptors(proto); for (var key in descriptors) { - if (descriptors[key].set) { + if ( + descriptors[key].set && + // better safe than sorry, we don't want spread attributes to mess with HTML content + key !== 'innerHTML' && + key !== 'textContent' && + key !== 'innerText' + ) { setters.push(key); } } diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/shared.js b/packages/svelte/src/internal/client/dom/elements/bindings/shared.js index aa083776a5..303890adec 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/shared.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/shared.js @@ -5,6 +5,7 @@ import { set_active_effect, set_active_reaction } from '../../../runtime.js'; +import { FORM_RESET_HANDLER } from '../../../constants.js'; import { add_form_reset_listener } from '../misc.js'; /** @@ -58,18 +59,15 @@ export function without_reactive_context(fn) { */ export function listen_to_event_and_reset_event(element, event, handler, on_reset = handler) { element.addEventListener(event, () => without_reactive_context(handler)); - // @ts-expect-error - const prev = element.__on_r; + const prev = /** @type {any} */ (element)[FORM_RESET_HANDLER]; if (prev) { // special case for checkbox that can have multiple binds (group & checked) - // @ts-expect-error - element.__on_r = () => { + /** @type {any} */ (element)[FORM_RESET_HANDLER] = () => { prev(); on_reset(true); }; } else { - // @ts-expect-error - element.__on_r = () => on_reset(true); + /** @type {any} */ (element)[FORM_RESET_HANDLER] = () => on_reset(true); } add_form_reset_listener(); diff --git a/packages/svelte/src/internal/client/dom/elements/class.js b/packages/svelte/src/internal/client/dom/elements/class.js index 063e67a70b..27c84e8f73 100644 --- a/packages/svelte/src/internal/client/dom/elements/class.js +++ b/packages/svelte/src/internal/client/dom/elements/class.js @@ -1,4 +1,5 @@ import { to_class } from '../../../shared/attributes.js'; +import { CLASS_CACHE } from '../../constants.js'; import { hydrating } from '../hydration.js'; import { class_list_toggle, @@ -17,8 +18,7 @@ import { * @returns {Record | undefined} */ export function set_class(dom, is_html, value, hash, prev_classes, next_classes) { - // @ts-expect-error need to add __className to patched prototype - var prev = dom.__className; + var prev = /** @type {any} */ (dom)[CLASS_CACHE]; if ( hydrating || @@ -41,8 +41,7 @@ export function set_class(dom, is_html, value, hash, prev_classes, next_classes) } } - // @ts-expect-error need to add __className to patched prototype - dom.__className = value; + /** @type {any} */ (dom)[CLASS_CACHE] = value; } else if (next_classes && prev_classes !== next_classes) { for (var key in next_classes) { var is_present = !!next_classes[key]; diff --git a/packages/svelte/src/internal/client/dom/elements/events.js b/packages/svelte/src/internal/client/dom/elements/events.js index 5885ff6d83..c0e2370179 100644 --- a/packages/svelte/src/internal/client/dom/elements/events.js +++ b/packages/svelte/src/internal/client/dom/elements/events.js @@ -261,9 +261,9 @@ export function handle_event_propagation(event) { }); // This started because of Chromium issue https://chromestatus.com/feature/5128696823545856, - // where removal or moving of of the DOM can cause sync `blur` events to fire, which can cause logic + // where removal or moving of the DOM can cause sync `blur` events to fire, which can cause logic // to run inside the current `active_reaction`, which isn't what we want at all. However, on reflection, - // it's probably best that all event handled by Svelte have this behaviour, as we don't really want + // it's probably best that all events handled by Svelte have this behaviour, as we don't really want // an event handler to run in the context of another reaction or effect. var previous_reaction = active_reaction; var previous_effect = active_effect; diff --git a/packages/svelte/src/internal/client/dom/elements/misc.js b/packages/svelte/src/internal/client/dom/elements/misc.js index 69620cfff1..cd8ba42fd7 100644 --- a/packages/svelte/src/internal/client/dom/elements/misc.js +++ b/packages/svelte/src/internal/client/dom/elements/misc.js @@ -1,6 +1,7 @@ import { hydrating } from '../hydration.js'; import { clear_text_content, get_first_child, add_event_listener } from '../operations.js'; import { queue_micro_task } from '../task.js'; +import { FORM_RESET_HANDLER } from '../../constants.js'; /** * @param {HTMLElement} dom @@ -46,8 +47,7 @@ export function add_form_reset_listener() { Promise.resolve().then(() => { if (!evt.defaultPrevented) { for (const e of /**@type {HTMLFormElement} */ (evt.target).elements) { - // @ts-expect-error - e.__on_r?.(); + /** @type {any} */ (e)[FORM_RESET_HANDLER]?.(); } } }); diff --git a/packages/svelte/src/internal/client/dom/elements/style.js b/packages/svelte/src/internal/client/dom/elements/style.js index b5b68eca28..a70403fdd4 100644 --- a/packages/svelte/src/internal/client/dom/elements/style.js +++ b/packages/svelte/src/internal/client/dom/elements/style.js @@ -1,4 +1,5 @@ import { to_style } from '../../../shared/attributes.js'; +import { STYLE_CACHE } from '../../constants.js'; import { hydrating } from '../hydration.js'; import { style_remove_property, @@ -35,8 +36,7 @@ function update_styles(dom, prev = {}, next, priority) { * @param {Record | [Record, Record]} [next_styles] */ export function set_style(dom, value, prev_styles, next_styles) { - // @ts-expect-error - var prev = dom.__style; + var prev = /** @type {any} */ (dom)[STYLE_CACHE]; if (hydrating || prev !== value) { var next_style_attr = to_style(value, next_styles); @@ -49,8 +49,7 @@ export function set_style(dom, value, prev_styles, next_styles) { } } - // @ts-expect-error - dom.__style = value; + /** @type {any} */ (dom)[STYLE_CACHE] = value; } else if (next_styles) { if (Array.isArray(next_styles)) { update_styles(dom, prev_styles?.[0], next_styles[0]); diff --git a/packages/svelte/src/internal/client/dom/operations.js b/packages/svelte/src/internal/client/dom/operations.js index 48b071568f..6a7ad24620 100644 --- a/packages/svelte/src/internal/client/dom/operations.js +++ b/packages/svelte/src/internal/client/dom/operations.js @@ -6,10 +6,14 @@ import { get_descriptor, is_extensible } from '../../shared/utils.js'; import { active_effect } from '../runtime.js'; import { async_mode_flag } from '../../flags/index.js'; import { - TEXT_NODE, + ATTRIBUTES_CACHE, + CLASS_CACHE, REACTION_RAN, - CUSTOM_RENDERER_NODE_TYPE_MAP, - COMMENT_NODE + STYLE_CACHE, + TEXT_CACHE, + TEXT_NODE, + COMMENT_NODE, + CUSTOM_RENDERER_NODE_TYPE_MAP } from '#client/constants'; import { eager_block_effects } from '../reactivity/batch.js'; import { NAMESPACE_HTML } from '../../../constants.js'; @@ -54,21 +58,15 @@ export function init_operations() { if (is_extensible(element_prototype)) { // the following assignments improve perf of lookups on DOM nodes - // @ts-expect-error - element_prototype.__click = undefined; - // @ts-expect-error - element_prototype.__className = undefined; - // @ts-expect-error - element_prototype.__attributes = null; - // @ts-expect-error - element_prototype.__style = undefined; + /** @type {any} */ (element_prototype)[CLASS_CACHE] = undefined; + /** @type {any} */ (element_prototype)[ATTRIBUTES_CACHE] = null; + /** @type {any} */ (element_prototype)[STYLE_CACHE] = undefined; // @ts-expect-error element_prototype.__e = undefined; } if (is_extensible(text_prototype)) { - // @ts-expect-error - text_prototype.__t = undefined; + /** @type {any} */ (text_prototype)[TEXT_CACHE] = undefined; } if (DEV) { diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index d822834324..a5c9a51eec 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -16,7 +16,8 @@ import { EAGER_EFFECT, ERROR_VALUE, MANAGED_EFFECT, - REACTION_RAN + REACTION_RAN, + DESTROYING } from '#client/constants'; import { async_mode_flag } from '../../flags/index.js'; import { deferred, define_property, includes } from '../../shared/utils.js'; @@ -33,7 +34,7 @@ import { flush_tasks, queue_micro_task } from '../dom/task.js'; import { DEV } from 'esm-env'; import { invoke_error_boundary } from '../error-handling.js'; import { flush_eager_effects, old_values, set_eager_effects, source, update } from './sources.js'; -import { eager_effect, unlink_effect } from './effects.js'; +import { eager_effect, teardown, unlink_effect } from './effects.js'; import { defer_effect } from './utils.js'; import { UNINITIALIZED } from '../../../constants.js'; import { set_signal_status } from './status.js'; @@ -1234,6 +1235,9 @@ function eager_flush() { }); } +/** @type {Map>} */ +var version_map = new Map(); + /** * Implementation of `$state.eager(fn())` * @template T @@ -1241,10 +1245,22 @@ function eager_flush() { * @returns {T} */ export function eager(fn) { - var version = source(0); var initial = true; var value = /** @type {T} */ (undefined); + if (active_reaction === null) { + return fn(); + } + + let parent = active_reaction; + + let version = version_map.get(parent) ?? source(0); + version_map.set(parent, version); + + teardown(() => { + if (parent.f & DESTROYING) version_map.delete(parent); + }); + get(version); eager_effect(() => { diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index b125ff0ddd..0278639951 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -30,7 +30,7 @@ import * as w from './warnings.js'; import * as e from './errors.js'; import { assign_nodes } from './dom/template.js'; import { is_passive_event } from '../../utils.js'; -import { COMMENT_NODE, STATE_SYMBOL } from './constants.js'; +import { COMMENT_NODE, STATE_SYMBOL, TEXT_CACHE } from './constants.js'; import { boundary } from './dom/blocks/boundary.js'; /** @@ -53,10 +53,9 @@ export function set_should_intro(value) { export function set_text(text, value) { // For objects, we apply string coercion (which might make things like $state array references in the template reactive) before diffing var str = value == null ? '' : typeof value === 'object' ? `${value}` : value; - // @ts-expect-error - if (str !== (text.__t ??= get_node_value(text))) { - // @ts-expect-error - text.__t = str; + // prettier-ignore + if (str !== (/** @type {any} */ (text)[TEXT_CACHE] ??= get_node_value(text))) { + /** @type {any} */ (text)[TEXT_CACHE] = str; set_node_value(text, `${str}`); } } diff --git a/packages/svelte/src/internal/server/hydratable.js b/packages/svelte/src/internal/server/hydratable.js index 81f59ab2fd..be75288604 100644 --- a/packages/svelte/src/internal/server/hydratable.js +++ b/packages/svelte/src/internal/server/hydratable.js @@ -3,7 +3,6 @@ import { async_mode_flag } from '../flags/index.js'; import { get_render_context } from './render-context.js'; import * as e from './errors.js'; import * as devalue from 'devalue'; -import { get_stack } from '../shared/dev.js'; import { DEV } from 'esm-env'; import { get_user_code_location } from './dev.js'; @@ -65,7 +64,13 @@ function encode(key, value, unresolved) { const placeholder = `"${uid++}"`; const p = value .then((v) => { - entry.serialized = entry.serialized.replace(placeholder, `r(${uneval(v)})`); + entry.serialized = entry.serialized.replace( + placeholder, + // use the function form here to prevent any string replacement characters from being interpreted + // in `v`, as it's potentially user-controlled and therefore potentially malicious. + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#specifying_a_string_as_the_replacement + () => `r(${uneval(v)})` + ); }) .catch((devalue_error) => e.hydratable_serialization_failed( diff --git a/packages/svelte/src/internal/server/hydratable.test.ts b/packages/svelte/src/internal/server/hydratable.test.ts new file mode 100644 index 0000000000..8dd54c8f1d --- /dev/null +++ b/packages/svelte/src/internal/server/hydratable.test.ts @@ -0,0 +1,33 @@ +import { afterAll, beforeAll, expect, test } from 'vitest'; +import { Renderer } from './renderer.js'; +import type { Component } from 'svelte'; +import { disable_async_mode_flag, enable_async_mode_flag } from '../flags/index.js'; +import { hydratable } from './hydratable.js'; + +beforeAll(() => { + enable_async_mode_flag(); +}); + +afterAll(() => { + disable_async_mode_flag(); +}); + +test('treats replacement tokens in hydratable promise values as literals', async () => { + const component = (renderer: Renderer) => { + hydratable('key', () => Promise.resolve(`$'`)); + renderer.child(async () => { + await Promise.resolve(); + }); + renderer.push('ok'); + }; + + const { head } = await Renderer.render(component as unknown as Component); + const script_match = head.match(/]*)?>([\s\S]*)<\/script>/); + + expect(script_match, 'expected hydratable script in head output').toBeTruthy(); + + const script_content = script_match![1]; + expect(script_content).toContain('const h = (window.__svelte ??= {}).h ??= new Map();'); + expect(script_content).toContain('r("$\'")'); + expect(script_content).toMatch(/\[\s*"key"\s*,\s*r\("\$'"\)\s*\]/); +}); diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 34d0133a31..12f76f188e 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -151,7 +151,7 @@ export function attributes(attrs, css_hash, classes, styles, flags = 0) { // omit functions, internal svelte properties and invalid attribute names if (typeof attrs[name] === 'function') continue; if (name[0] === '$' && name[1] === '$') continue; // faster than name.startsWith('$$') - if (INVALID_ATTR_NAME_CHAR_REGEX.test(name)) continue; + if (name === '' || INVALID_ATTR_NAME_CHAR_REGEX.test(name)) continue; var value = attrs[name]; var lower = name.toLowerCase(); diff --git a/packages/svelte/src/utils.js b/packages/svelte/src/utils.js index 57561e6dc7..54757a6f13 100644 --- a/packages/svelte/src/utils.js +++ b/packages/svelte/src/utils.js @@ -491,7 +491,7 @@ export function is_raw_text_element(name) { // 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; + /^[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 diff --git a/packages/svelte/src/utils.test.ts b/packages/svelte/src/utils.test.ts new file mode 100644 index 0000000000..02175ae93f --- /dev/null +++ b/packages/svelte/src/utils.test.ts @@ -0,0 +1,50 @@ +import { expect, test } from 'vitest'; +import { REGEX_VALID_TAG_NAME } from './utils'; + +test('REGEX_VALID_TAG_NAME accepts common HTML tag names', () => { + const common_html_tag_names = ['div', 'span', 'button', 'input', 'svg', 'math', 'a']; + + for (const tag_name of common_html_tag_names) { + expect(REGEX_VALID_TAG_NAME.test(tag_name)).toBe(true); + } +}); + +test('REGEX_VALID_TAG_NAME accepts basic custom element names', () => { + const valid_custom_tag_names = ['my-element', 'x-foo', 'todo-item', 'my-element2']; + + for (const tag_name of valid_custom_tag_names) { + expect(REGEX_VALID_TAG_NAME.test(tag_name)).toBe(true); + } +}); + +test('REGEX_VALID_TAG_NAME accepts spec-allowed custom element characters', () => { + const valid_custom_tag_names = [ + 'x-foo.bar', + 'x-foo_bar', + 'x-foo\u00B7bar', + 'x-foo\u00FCbar', + 'x-foo\u{1F600}bar', + 'x-' + ]; + + for (const tag_name of valid_custom_tag_names) { + expect(REGEX_VALID_TAG_NAME.test(tag_name)).toBe(true); + } +}); + +test('REGEX_VALID_TAG_NAME rejects invalid tag names', () => { + const invalid_tag_names = ['', '1', 'x\u00FC', '-x-foo', '1foo', 'x-foo bar', 'x-foo/', 'x-foo>']; + + for (const tag_name of invalid_tag_names) { + expect(REGEX_VALID_TAG_NAME.test(tag_name)).toBe(false); + } +}); + +test('REGEX_VALID_TAG_NAME no ReDoS', () => { + const before = performance.now(); + REGEX_VALID_TAG_NAME.test('a-----------------------------------!'); + const after = performance.now(); + if (after - before > 10) { + throw new Error(`REGEX_VALID_TAG_NAME is vulnerable to ReDoS`); + } +}); diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 04b0b0398a..6b0ed0aea8 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.55.5'; +export const VERSION = '5.55.7'; export const PUBLIC_VERSION = '5'; diff --git a/packages/svelte/tests/runtime-browser/samples/dom-clobbering-cache-symbols-spread/_config.js b/packages/svelte/tests/runtime-browser/samples/dom-clobbering-cache-symbols-spread/_config.js new file mode 100644 index 0000000000..62248851db --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/dom-clobbering-cache-symbols-spread/_config.js @@ -0,0 +1,23 @@ +import { assert_ok, test } from '../../assert'; + +export default test({ + async test({ assert, target, waitUntil, window }) { + const form = target.querySelector('form'); + const button = target.querySelector('button'); + const [i1, i2, i3] = target.querySelectorAll('input'); + assert_ok(form); + assert_ok(button); + + assert.equal(form.id, 'initial-form'); + assert.equal(form.className, 'first'); + assert.equal(window.getComputedStyle(form).backgroundColor, 'rgb(255, 0, 0)'); + + button.click(); + await waitUntil(() => form.id === 'updated-form'); + + assert.equal(form.id, 'updated-form'); + assert.equal(form.className, 'second'); + assert.equal(i3.id, '', 'input clobbered form'); + assert.equal(window.getComputedStyle(form).backgroundColor, 'rgb(0, 0, 255)'); + } +}); diff --git a/packages/svelte/tests/runtime-browser/samples/dom-clobbering-cache-symbols-spread/main.svelte b/packages/svelte/tests/runtime-browser/samples/dom-clobbering-cache-symbols-spread/main.svelte new file mode 100644 index 0000000000..60e9b80e81 --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/dom-clobbering-cache-symbols-spread/main.svelte @@ -0,0 +1,20 @@ + + + + +
+ + + + +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-eager-block/_config.js b/packages/svelte/tests/runtime-runes/samples/async-eager-block/_config.js new file mode 100644 index 0000000000..b6d283c8a5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-eager-block/_config.js @@ -0,0 +1,23 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + const [increment, resolve] = target.querySelectorAll('button'); + + increment.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ' 0

loading...

' + ); + + resolve.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ' 1

1

' + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-eager-block/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-eager-block/main.svelte new file mode 100644 index 0000000000..61f4705d06 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-eager-block/main.svelte @@ -0,0 +1,20 @@ + + + + + +{await delay(count)} +{#if $state.eager(count) !== count} +

loading...

+{:else} +

{count}

+{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-eager-each-block/_config.js b/packages/svelte/tests/runtime-runes/samples/async-eager-each-block/_config.js new file mode 100644 index 0000000000..a4921a5115 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-eager-each-block/_config.js @@ -0,0 +1,32 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, logs }) { + await tick(); + const [increment, resolve] = target.querySelectorAll('button'); + logs.length = 0; + + increment.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` +
  • 0 / 0
  • 0 / loading...
  • 0 / 0
` + ); + + resolve.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` +
  • 0 / 0
  • 1 / 1
  • 0 / 0
` + ); + + assert.equal( + logs.some((l) => l.toString().includes('0 ') || l.toString().includes('2')), + false, + 'only the second $state.eager should have been evaluated' + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-eager-each-block/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-eager-each-block/main.svelte new file mode 100644 index 0000000000..f27e256e9f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-eager-each-block/main.svelte @@ -0,0 +1,24 @@ + + + + + +
    + {#each counts as count, i} +
  • + {await delay(count)} / + {#if console.log(i) || $state.eager(count) !== count} + loading... + {:else} + {count} + {/if} +
  • + {/each} +
diff --git a/packages/svelte/tests/server-side-rendering/samples/spread-attributes-event-handler-xss/main.svelte b/packages/svelte/tests/server-side-rendering/samples/spread-attributes-event-handler-xss/main.svelte index 2a8c17f048..e47a19408e 100644 --- a/packages/svelte/tests/server-side-rendering/samples/spread-attributes-event-handler-xss/main.svelte +++ b/packages/svelte/tests/server-side-rendering/samples/spread-attributes-event-handler-xss/main.svelte @@ -5,7 +5,12 @@ onclick: 'alert(1)', onerror: 'alert(1)', onfocus: 'alert(1)', - onmouseover: 'alert(1)' + onmouseover: 'alert(1)', + ' onload': 'alert(1)', + '\tonload': 'alert(1)', + '\u00a0onload': 'alert(1)', + "\t": "/onmouseover=alert(1)//", + "": "/onmouseover=alert(1)//" }; diff --git a/playgrounds/sandbox/ssr-dev.js b/playgrounds/sandbox/ssr-dev.js index 8a0c063d47..b770848ad1 100644 --- a/playgrounds/sandbox/ssr-dev.js +++ b/playgrounds/sandbox/ssr-dev.js @@ -29,8 +29,9 @@ polka() const { head, body } = await render(App); const html = transformed_template - .replace(``, head) - .replace(``, body) + // use function form to prevent any string replacement characters from being interpreted + .replace(``, () => head) + .replace(``, () => body) // check that Safari doesn't break hydration .replaceAll('+636-555-3226', '+636-555-3226'); diff --git a/playgrounds/sandbox/ssr-prod.js b/playgrounds/sandbox/ssr-prod.js index 0c760188d4..e3ece4d5be 100644 --- a/playgrounds/sandbox/ssr-prod.js +++ b/playgrounds/sandbox/ssr-prod.js @@ -9,8 +9,9 @@ const { head, body } = await render(App); const rendered = fs .readFileSync(path.resolve('./dist/client/index.html'), 'utf-8') - .replace(``, body) - .replace(``, head); + // use function form to prevent any string replacement characters from being interpreted + .replace(``, () => body) + .replace(``, () => head); const types = { '.js': 'application/javascript', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0e2be208b6..1bfee5c769 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -96,8 +96,8 @@ importers: specifier: ^2.1.1 version: 2.1.1 devalue: - specifier: ^5.6.4 - version: 5.6.4 + specifier: ^5.8.1 + version: 5.8.1 esm-env: specifier: ^1.2.1 version: 1.2.1 @@ -1418,8 +1418,8 @@ packages: engines: {node: '>=0.10'} hasBin: true - devalue@5.6.4: - resolution: {integrity: sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==} + devalue@5.8.1: + resolution: {integrity: sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==} dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} @@ -3786,7 +3786,7 @@ snapshots: detect-libc@1.0.3: optional: true - devalue@5.6.4: {} + devalue@5.8.1: {} dir-glob@3.0.1: dependencies: