CVE-2025-15265

Svelte 5 SSR XSS: Poisoning the Hydration Well

Amit Schendel
Amit Schendel
Senior Security Researcher

Jan 16, 2026·6 min read

Executive Summary (TL;DR)

Svelte 5.46.0-5.46.2 used `JSON.stringify` to serialize hydration keys in SSR responses. Because `JSON.stringify` does not escape the `</script>` sequence, an attacker can supply a malicious key that terminates the legitimate script block and injects arbitrary JavaScript, leading to XSS.

A critical Cross-Site Scripting (XSS) vulnerability in Svelte 5's Server-Side Rendering (SSR) engine allows attackers to break out of hydration scripts using crafted keys.

The Hook: When Magic Spells Backfire

Svelte 5 is the new hotness in the frontend world. It promises 'runes', fine-grained reactivity, and a developer experience smooth enough to slide on. One of the mechanisms making this magic happen is hydration—the process where the client-side JavaScript takes over the static HTML rendered by the server. In Svelte 5, a specific function called hydratable was introduced to handle asynchronous data synchronization. It essentially says, "Hey client, I started fetching this data on the server; here is the key to find it when you wake up."

To make this handoff work, Svelte injects a script tag into the HTML head, populating a global Map (window.__svelte.h) with these keys and their corresponding data. It’s a standard technique: serialize data on the server, print it to the DOM, and read it on the client. It’s efficient, it’s fast, and in versions 5.46.0 through 5.46.2, it was dangerously broken.

The vulnerability lies in a subtle disconnect between how JavaScript defines a string and how an HTML parser defines a script block. The developers used a standard JSON serializer to write user-controlled keys into the page. Unfortunately, the browser's HTML parser is a brute that doesn't care about your JavaScript string literals. If it sees a specific sequence of characters, it drops the hammer, and that gives us our entry point.

The Flaw: The 'End Tag Open' Trap

The root cause is a classic web security pop-quiz question: Is JSON.stringify() safe to use inside an HTML <script> block?

The answer is a resounding no. While JSON.stringify correctly escapes double quotes (") and backslashes (\), it does not escape the characters < or /. This becomes a critical flaw due to how browsers parse HTML.

When the browser's HTML tokenizer encounters a <script> tag, it switches to a "script data" state. It stays in this state until it sees the closing tag: </script>. Crucially, the HTML parser does not understand JavaScript syntax. It doesn't know what a string is. It doesn't know what a comment is. It only looks for that closing tag.

So, if your serialized JSON string looks like this:

var data = "User input: </script><script>alert(1)</script>";

The JavaScript engine sees a string containing HTML tags. But the HTML parser sees the first </script> and immediately closes the script block. The remaining characters—<script>alert(1)</script>—are then interpreted as raw HTML. The first script block (now containing a syntax error because of the truncated string) fails silently or errors out, and the attacker's injected script tag executes immediately afterwards.

The Code: The Smoking Gun

Let's look at the crime scene in packages/svelte/src/internal/server/renderer.js. The Svelte SSR engine iterates over hydration entries and pushes them into an array to be written to the document. In the vulnerable versions, the code looked like this:

// Vulnerable Code (Svelte 5.46.0 - 5.46.2)
// 'k' is the key (potentially user-controlled)
// 'v' is the value
entries.push(`[${JSON.stringify(k)},${v.serialized}]`);

The reliance on JSON.stringify(k) is the fatal error here. If k contains </script>, it is written literally into the output. The fix implemented in version 5.46.3 swaps this out for devalue, a library specifically designed to handle this exact scenario (and others, like circular references).

// Patched Code (Svelte 5.46.3+)
import * as devalue from 'devalue';
 
// devalue.uneval() properly escapes unsafe HTML characters
entries.push(`[${devalue.uneval(k)},${v.serialized}]`);

devalue.uneval ensures that characters like < are escaped (e.g., to \u003C), preventing the HTML parser from premature ejaculation of the script block.

The Exploit: Breaking Out

Exploiting this requires finding a Svelte application that uses hydratable with a key derived from untrusted input. A common pattern is using a URL slug, ID, or query parameter as the hydration key to cache a data fetch.

Imagine a component like this:

<script>
  import { hydratable } from "svelte";
  // 'key' is derived from ?id=... in the URL
  let { key } = $props();
  const value = await hydratable(key, fetchPost);
</script>

An attacker sends a victim a link with the following payload: https://example.com/post?id=</script><script>alert('Pwned')</script>

When the server renders this, it generates the following HTML:

<script>
  const h = (window.__svelte ??= {}).h ??= new Map();
  h.set("KEY_HERE", ...);
  // Becomes:
  h.set("</script><script>alert('Pwned')</script>", ...);
</script>

Here is a visualization of the parse flow:

The browser sees the red text as the end of the script. The subsequent <script> tag is valid HTML, and the XSS fires.

The Impact: Why Panic?

This is a Reflected XSS vulnerability, but because it happens during Server-Side Rendering, it has some nuance. Unlike client-side XSS where a framework might sanitize data before rendering it to the DOM, this injection happens at the raw HTML generation level.

Successful exploitation allows for:

  1. Session Hijacking: Stealing session cookies (if not HttpOnly).
  2. CSRF on Steroids: Performing actions on the site as the victim without their consent.
  3. Phishing: Rewriting the page content to display a fake login form.

What makes this particularly spicy is that the injection occurs in the <head> or very early in the <body>, often before the main application logic has even loaded. Security controls that rely on JavaScript execution to initialize might not even be running yet when the payload fires.

The Fix: Use the Right Tools

The mitigation is straightforward: Stop using JSON.stringify to generate inline script content.

If you are a user of Svelte 5:

  1. Upgrade immediately to 5.46.3 or later. The Svelte team squashed this quickly.
  2. Audit your codebase for any manual SSR logic where you might be doing res.send('<script>var x = ' + JSON.stringify(input) + '</script>'). This pattern is almost always vulnerable.

If you are a developer writing SSR logic in any language: Always use a serialization library that is aware of the HTML context. In the Node.js/JS ecosystem, libraries like devalue, jsesc, or serialize-javascript are your friends. They turn < into \u003C, ensuring the browser never mistakes your data for a tag.

Fix Analysis (1)

Technical Appendix

CVSS Score
5.3/ 10
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N

Affected Systems

Svelte 5.46.0Svelte 5.46.1Svelte 5.46.2

Affected Versions Detail

Product
Affected Versions
Fixed Version
Svelte
Svelte
>= 5.46.0 < 5.46.35.46.3
AttributeDetail
CWE IDCWE-79
Attack VectorNetwork
CVSS Base5.3 (Medium)
ImpactCross-Site Scripting (XSS)
Affected ComponentSSR Hydration Engine
Root CauseUnsafe serialization of user input in script tags
CWE-79
Cross-site Scripting

Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')

Vulnerability Timeline

Vulnerability fixed in version 5.46.3
2026-01-15
Advisory Published by Svelte Team
2026-01-15

Subscribe to updates

Get the latest CVE analysis reports delivered to your inbox.