GHSA-F3CJ-J4F6-WQ85: Root-cause fix for hydratable SSR XSS in Svelte
Summary
The patch addresses an SSR XSS flaw in Svelte's hydratable promise serialization by switching String.prototype.replace from string-replacement mode to callback mode. This prevents attacker-controlled replacement tokens such as $' from being interpreted during placeholder substitution. The added regression test directly covers the reported primitive, and the same defensive pattern is applied in sandbox SSR template replacement code.
Analysis
Vulnerability
GHSA-F3CJ-J4F6-WQ85 describes a server-side rendering cross-site scripting issue in Svelte versions prior to 5.55.7 affecting the hydratable SSR path for promises. The vulnerable code performed placeholder substitution with String.prototype.replace using a string replacement value derived from uneval(v), where v could be user-controlled.
In JavaScript, string-mode replacements interpret special replacement patterns such as $&, $`, $', and capture references. That means a payload containing $' is not treated as inert data during replacement; instead, it can splice in the post-match suffix of the serialized script. In an SSR hydration script, this can corrupt the generated JavaScript and create an XSS primitive in the browser.
The commit at the upstream patch shows the vulnerable line in packages/svelte/src/internal/server/hydratable.js:
entry.serialized = entry.serialized.replace(placeholder, `r(${uneval(v)})`);This is a classic misuse of replace: the escaping/serialization of the value itself may be correct, but the replacement API introduces a second interpretation layer. The root cause is therefore not generic output encoding failure; it is unsafe use of string replacement semantics on attacker-influenced content.
Patch
The patch changes the replacement call to callback form so the returned string is inserted literally rather than parsed for replacement tokens:
entry.serialized = entry.serialized.replace(
placeholder,
() => `r(${uneval(v)})`
);This is the correct mitigation for this bug class. Callback-mode replace bypasses replacement-token expansion, so payloads like $' remain data. The inline comment in the patch explicitly documents this behavior and links to the relevant MDN semantics in the source commit at the GitHub patch reference.
The patch also adds a regression test in packages/svelte/src/internal/server/hydratable.test.ts that exercises the exact dangerous token:
hydratable('key', () => Promise.resolve(`$'`));
...
expect(script_content).toContain('r("$\'")');That test is important because it validates the API-level security property: replacement tokens in hydratable promise values must be treated as literals in the emitted hydration script.
Additionally, the same defensive pattern is applied in the sandbox SSR files:
.replace(`<!--ssr-head-->`, () => head)
.replace(`<!--ssr-body-->`, () => body)Those changes are not the primary advisory fix, but they are consistent hardening against the same replacement-token hazard.
Review
Pros
- The fix directly addresses the root cause: string replacement token interpretation in
String.prototype.replace. - The chosen remediation is minimal and semantically correct. Using a replacement callback is the standard safe pattern when replacement content may contain
$-sequences. - The regression test targets the reported exploit primitive,
$', rather than only asserting generic rendering behavior. - The patch preserves existing serialization flow and does not appear to alter hydration protocol or data model behavior beyond eliminating the unsafe replacement semantics.
- Related sandbox SSR code is hardened with the same pattern, reducing recurrence of this bug class elsewhere in the codebase.
Cons
- The test coverage shown is narrowly focused on
$'. While that is the reported vector, additional cases such as$&,$`,$$, and capture-like sequences would better lock down the full replacement-token surface. - The patch fixes the immediate sink but does not indicate any broader audit of string-mode
.replace(..., userControlledString)usage across the SSR codebase. - The sandbox changes are useful hardening, but they may blur the boundary between advisory-relevant code and ancillary cleanup unless release notes clearly distinguish them.
Verdict
Root-cause.
This patch fixes the actual vulnerability mechanism rather than merely filtering one payload shape. The vulnerable behavior came from passing attacker-influenced serialized content as a string replacement argument, which activates JavaScript replacement-token expansion. Switching to callback-form replacement removes that interpretation layer entirely, so the dangerous token is rendered literally. Based on the provided diff and test, this is a technically sound and durable fix for the issue described in the advisory and implemented in the upstream commit. A follow-up audit for similar replace misuse patterns would still be prudent.
Recommended Labs
Try this vulnerability pattern yourself with hands-on labs.
- XSS.js
Best general starting point for this GHSA because it is a JavaScript hands-on lab focused on XSS/CWE-79. It helps build the core defensive skills relevant to server-side rendering output handling, untrusted data rendering, and safe escaping/encoding.
- Self Sabotage.js
Useful follow-up for practicing a more in-depth JavaScript XSS mitigation scenario. Since the Svelte issue involves subtle rendering behavior in hydratable SSR promises, a medium-difficulty JavaScript lab is a good fit for strengthening patch-review and defense-in-depth thinking.
- React XSS3.js
Although not Svelte-specific, this frontend-focused XSS lab is highly relevant because the vulnerability is in a modern UI framework’s SSR/hydration pathway. It can help transfer defensive lessons around component rendering, framework abstractions, and safely handling attacker-controlled content in browser-facing output.