Feb 17, 2026·6 min read·6 visits
Svelte versions prior to 3.59.2 failed to escape user input bound to `<textarea>` elements during server-side rendering. This allowed attackers to close the textarea tag prematurely and inject malicious scripts that execute immediately upon page load. The fix involves wrapping the output in an escape function.
A high-severity Cross-Site Scripting (XSS) vulnerability was discovered in the Svelte framework's Server-Side Rendering (SSR) engine. By exploiting improper escaping in `bind:value` directives on `<textarea>` elements, attackers can break out of the HTML tag context and inject arbitrary JavaScript. This affects applications rendering Svelte components on the server (e.g., SvelteKit) prior to version 3.59.2.
We love Svelte. It's the framework that isn't a framework. It compiles your code away into tiny, efficient, imperative JavaScript. It promises to handle the heavy lifting, the reactivity, and—crucially—the security. We trust the compiler to sanitize our inputs because, frankly, writing document.createElement by hand in 2024 is a form of masochism reserved for people who enjoy writing assembly.
But here's the thing about Server-Side Rendering (SSR): it's not DOM manipulation. It is, at its core, sophisticated string concatenation. The Svelte compiler takes your components and turns them into a function that spits out a long HTML string to send to the browser. And whenever you are concatenating strings to build HTML, you are dancing on the edge of a volcano.
This vulnerability, living in the shadows of the <textarea> tag, is a reminder that even the smartest compilers can slip up on the most basic HTML rules. It's a classic case of "it works on my machine (client-side)" failing catastrophically when moved to the server.
To understand this bug, you have to understand how browsers parse HTML. Most input elements use the value attribute (e.g., <input value="...">). Attributes are easy; you quote them, you escape quotes, and you move on.
But <textarea> is a weird beast. It doesn't use a value attribute. Its value is the text content between the opening and closing tags. In HTML parsing terms, this is often treated as RCDATA (Raw Text). The browser generally doesn't parse markup inside a textarea... until it sees a closing tag.
The vulnerability lies in src/compiler/compile/render_ssr/handlers/Element.ts. When Svelte compiled a component with <textarea bind:value={userInput}> for SSR, it essentially generated code that said: "Print <textarea>, then print userInput, then print </textarea>."
Do you see the problem? It didn't sanitize userInput. It assumed that because it was inside a textarea, it was safe. But if userInput contains </textarea>, the browser stops parsing the textarea context immediately and treats everything following it as raw HTML. The compiler was locking the front door but leaving the keys under the mat.
Let's look at the actual compiler code. This is where the magic (and the mistake) happened. The compiler is written in TypeScript and generates JavaScript string templates.
The Vulnerable Code (Pre-3.59.2):
// src/compiler/compile/render_ssr/handlers/Element.ts
} else if (binding.name === 'value' && node.name === 'textarea') {
const snippet = expression.node;
// FATAL FLAW: The snippet is injected directly without escaping
node_contents = x`${snippet} || ""`;
}The variable node_contents is what eventually gets shoved between the <textarea> tags. The x literal tag function is just a helper for generating code. The crucial part is ${snippet} || "". It takes whatever variable you bound to the textarea and dumps it into the HTML string.
The Fix (Commit a31dec5):
// src/compiler/compile/render_ssr/handlers/Element.ts
} else if (binding.name === 'value' && node.name === 'textarea') {
const snippet = expression.node;
// FIX: Wrap the content in the @escape macro
node_contents = x`@escape(${snippet} || "")`;
}The fix is elegantly simple. They wrapped the expression in @escape(...). This internal Svelte function converts characters like < to < and > to >. So, if an attacker tries to inject </textarea>, it becomes </textarea>, which the browser safely renders as text inside the box rather than parsing it as a tag closer.
Exploiting this is trivial if you can control data that gets pre-rendered on the server. Imagine a user profile page where you can set a "Bio". This bio is rendered into a generic <textarea> so the user can edit it later.
The Setup:
<textarea bind:value={userBio}></textarea>sometext</textarea><script>alert('pwned')</script>The Execution: When the server renders this page, it blindly concatenates the strings. The HTML sent to the victim looks like this:
<textarea>sometext</textarea><script>alert('pwned')</script></textarea>The Browser's Perspective:
<textarea>: "Okay, I'm starting a text box."sometext: "Putting this in the text box."</textarea>: "Okay, closing the text box."<script>...: "Oh, a script tag! I should execute this immediately."</textarea>: "Weird floating closing tag. I'll ignore it or show it in the DOM."Boom. XSS. The script executes as soon as the HTML is parsed, allowing cookie theft, session hijacking, or redirection.
Since this is an SSR vulnerability, the XSS payload is delivered in the initial HTML document. This is often more dangerous than client-side reflected XSS because:
It breaks the trust model of the framework. Developers use Svelte specifically because they expect bind:value to be safe by default. This bug silently turned a standard feature into a security hole.
The remediation is straightforward, but mandatory.
1. Upgrade Svelte
If you are on version 3.x, you must upgrade to 3.59.2 or later. If you are already on Svelte 4 or 5, you are safe (the fix was carried forward). Check your package.json and package-lock.json.
npm update svelte
# Check version
npm list svelte2. Defensive Coding (The Paranoia approach) Even with the patch, it is good hygiene to treat all user input as radioactive. If you are rendering user content on the server, ensure you have input validation layers (like Zod or Joi) active before the data even reaches the component tree. Never rely solely on the view layer to sanitize your database's dirty secrets.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
svelte Svelte | >= 3.0.0, < 3.59.2 | 3.59.2 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-79 (Cross-Site Scripting) |
| Attack Vector | Network |
| CVSS | 7.5 (High) |
| Affected Component | Svelte SSR Compiler (Textarea Handler) |
| Impact | Code Execution (Browser Context) |
| Exploit Status | PoC Available |