Feb 27, 2026·5 min read·8 visits
Svelte's SSR compiler failed to escape data bound to `innerText` and `textContent` on `contenteditable` elements. This allows attackers to inject malicious HTML/JS scripts that execute when the page is rendered on the server. Fixed in v5.53.5.
In the world of web security, `innerText` is supposed to be the good guy—the safe alternative to the chaotic evil of `innerHTML`. Developers are taught that assigning text to `innerText` automatically escapes HTML entities, neutralizing Cross-Site Scripting (XSS) attacks before they start. But CVE-2026-27901 flips the script. In Svelte versions prior to 5.53.5, the Server-Side Rendering (SSR) engine treated `bind:innerText` on `contenteditable` elements as raw HTML injection. This effectively turned a safety feature into a direct pipeline for remote code execution in the victim's browser, proving once again that in SSR, everything is just string concatenation waiting to go wrong.
We often forget that Server-Side Rendering (SSR) is, at its core, a glorified string concatenation engine. When you write a component in a modern framework like Svelte, you aren't manipulating a DOM; you are instructing a compiler to build a massive string of HTML to chuck at the browser. In the browser (the client), the DOM API is your bodyguard. If you set element.innerText = "<script>", the browser dutifully converts that to <script>. You are safe.
But on the server? There is no DOM. There is only text. The framework has to simulate that safety by manually running escape functions on every piece of dynamic data before stitching it into the HTML string. If the framework developers miss just one spot, that safety evaporates.
This vulnerability is a classic case of that simulation breaking down. Svelte's compiler, usually rigorous about sanitization, had a blind spot for contenteditable elements. It assumed that if you were binding data to an editable area, you probably wanted the raw content, effectively treating innerText (the safe one) exactly like innerHTML (the dangerous one). It’s like labeling a bottle of nitroglycerin "Water" because they're both liquids.
The root cause lies deep within the Svelte compiler's transformation phase, specifically in how it handles attributes during SSR. The logic serves to take your high-level Svelte code and transpile it into efficient JavaScript that generates HTML strings.
When the compiler encountered a contenteditable element, it had to decide how to render the initial state. The logic checked if the binding was for innerText, textContent, or innerHTML. Tragically, the code grouped all three together. It assumed that because contenteditable allows rich text editing in some contexts, the developer must want the raw HTML output for all bindings.
This is a logic error, not a buffer overflow or a memory corruption. The compiler simply failed to apply the $.escape() utility to innerText and textContent bindings. It passed the raw expression directly into the final HTML output buffer. So, when the server renders <div contenteditable bind:innerText={userInput}>, it doesn't output <div><script>...</div>; it outputs <div><script>...</div>.
Let's look at the code responsible for this betrayal. The vulnerability lived in packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js. This file dictates how elements are constructed during the server-side build.
Before the fix, the code looked something like this:
// The "Oops" Moment
if (is_content_editable_binding(attribute.name)) {
// Whether it's innerHTML, innerText, or textContent,
// it just dumps the raw expression into content.
content = expression;
}It’s almost insultingly simple. The fix involves explicitly checking the attribute name and applying the escape function if it's not innerHTML.
After the fix (v5.53.5):
if (attribute.name === 'innerHTML') {
// innerHTML stays raw (dangerous by design)
content = expression;
} else if (
is_content_editable_binding(attribute.name) ||
(attribute.name === 'value' && node.name === 'textarea')
) {
// EVERYTHING ELSE GETS ESCAPED
content = b.call('$.escape', expression);
}That b.call('$.escape', expression) is the difference between a functional website and a compromised user base.
Exploiting this requires a scenario where you render user-controlled input into a contenteditable div using SSR. This is common in collaborative editing apps, CMS interfaces, or rich-text comment sections that want to be SEO-friendly.
Here is the attack chain:
<img src=x onerror=alert(document.cookie)>.Vulnerable Component:
<script>
let bio = $props.bio; // "<img src=x onerror=..."
</script>
<!-- The trap is set -->
<div contenteditable bind:innerText={bio}></div>The Execution: Svelte generates the HTML string. Because of the bug, the output is:
<div contenteditable><img src=x onerror=alert(document.cookie)></div>
The Payload: The victim's browser parses this HTML. It sees the img tag, tries to load x, fails, and triggers the onerror event, executing the JavaScript. The attacker now has the victim's session tokens.
While the CVSS score is a "Medium" 5.3, do not let that lull you into a false sense of security. The score is dampened by the complexity (AC:H) required—specifically, you need a contenteditable element bound to user input and SSR enabled. That is not every app, but it is exactly the kind of setup used by modern, rich web applications (think Notion clones, document editors, or social platforms).
If you are hit by this, the impact is full Stored XSS. This isn't just an alert box; it's session hijacking, forced actions, or phishing via DOM manipulation. Furthermore, because the payload comes from the server's initial HTML response, it executes immediately upon page load, often before client-side frameworks or hydration logic can intervene. It bypasses any client-side sanitization logic you might have running in onMount because the damage is done before the component even mounts.
The remediation is straightforward: Update svelte to version 5.53.5 immediately.
If you cannot update right now (perhaps you enjoy living dangerously or have strict change freezes), you must audit your codebase for bind:innerText or bind:textContent on elements that also have the contenteditable attribute.
Workaround: Stop binding directly. Instead, sanitize the data before passing it to the component context, or use a manual binding approach that doesn't rely on the vulnerable SSR path. But really, just update the package. It's a patch-level update designed specifically to fix this.
CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:P/VC:L/VI:N/VA:N/SC:H/SI:H/SA:N| Product | Affected Versions | Fixed Version |
|---|---|---|
svelte sveltejs | < 5.53.5 | 5.53.5 |
| Attribute | Detail |
|---|---|
| CVE ID | CVE-2026-27901 |
| CWE ID | CWE-79 (XSS) |
| CVSS Score | 5.3 (Medium) |
| Attack Vector | Network |
| Impact | Cross-Site Scripting (XSS) |
| Fixed Version | 5.53.5 |
Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')