Feb 20, 2026·5 min read·4 visits
The `devalue` library failed to block the `__proto__` key during object serialization. An attacker can supply a malicious JSON object which, when processed by `devalue` and subsequently executed by a JavaScript engine, modifies the prototype of the resulting object. This affects versions prior to 5.6.3.
A high-severity Prototype Pollution vulnerability has been discovered in `devalue`, a popular library used for serializing JavaScript values, particularly in Server-Side Rendering (SSR) contexts like Svelte and Nuxt. The flaw allows attackers to inject `__proto__` properties into serialized objects. When these objects are deserialized (typically via execution on the client side), the global `Object.prototype` becomes polluted, potentially leading to Denial of Service (DoS), Cross-Site Scripting (XSS), or logic bypasses in the target application.
Let's talk about "Hydration." It's that magical moment in modern web development where the server hands off a static HTML page to the browser and says, "Here, you take over." To do this, the server needs to send the current state of the application—JavaScript objects, arrays, maybe even a Date or a RegExp—down the wire.
JSON.stringify is the boring, safe uncle at the party. He only handles primitives. He chokes on circular references, hates undefined, and doesn't know what a Map is. Enter devalue. It's the cool cousin that says, "Don't worry, I can serialize anything into executable JavaScript code."
And therein lies the rub. devalue doesn't just produce a data string; it produces code that is meant to be evaluated (often via eval or a new Function constructor implicitly). When you turn user input into executable code, you are dancing on the edge of a volcano. In this case, the volcano erupted because devalue forgot that JavaScript objects have a very special, very dangerous property lurking in the shadows.
The vulnerability stems from a fundamental quirk in JavaScript: the difference between how JSON.parse handles __proto__ and how the JavaScript engine handles object literals.
If you run JSON.parse('{"__proto__": {"a": 1}}'), you get a plain object with a key named __proto__. Safe. Boring. The prototype remains untouched because JSON is just data.
However, devalue takes that object and converts it into a string of JavaScript code to be run later. It iterates over the keys and builds a string like { __proto__: { a: 1 } }. When the browser executes this string, it sees an Object Literal. In this context, __proto__ is NOT just a key—it is a magical setter that mutates the object's prototype chain.
Because devalue didn't explicitly forbid this key, it acted as a bridge. It took safe data (from JSON.parse) and transformed it into a weapon (an Object Literal prototype setter). The moment that code executes on the client, boom—Object.prototype is polluted.
Let's look at the "before" and "after". The vulnerability existed because the library simply iterated over keys without asking questions. It was trusting. It assumed you wouldn't hand it a grenade.
The fix, applied in commit 0f04d4d678eac39ad5d7a07d1956275d7874e81c, is a classic "stop and scream" pattern. Instead of blindly looping, the maintainers introduced a hard check for the forbidden key.
Here is the essence of the patch:
// THE FIX (Simplified)
// src/stringify.js & src/uneval.js
const keys = Object.keys(thing);
for (const key of keys) {
// The new security bouncer
if (key === '__proto__') {
throw new DevalueError(
`Cannot stringify objects with __proto__ keys`,
keys,
thing,
value
);
}
// ... proceed with serialization
}Notice the switch to Object.keys()? That's intentional. It ensures we are only looking at own properties. But the real hero is the if (key === '__proto__') check. It doesn't try to sanitize it. It doesn't try to be clever. It just throws an error and aborts the process. In security, failing fast is usually safer than trying to fix bad input.
So, how do we weaponize this? We need a scenario where a server accepts JSON input, deserializes it, passes it to devalue, and sends the result to a client.
The attacker sends a POST request with a JSON body. This is valid JSON, so the server accepts it without complaint.
{
"userInput": {
"__proto__": {
"isAdmin": true,
"vulnerable": true
}
}
}The server parses this JSON. Now we have an object in memory with a key __proto__. The server then uses devalue(userInput) to prepare it for the client. devalue generates this string:
{__proto__:{isAdmin:true,vulnerable:true}}This string is embedded in the HTML sent to the client, usually inside a <script> tag for hydration:
<script>
// Hydration logic
var data = {__proto__:{isAdmin:true,vulnerable:true}};
// At this exact moment, Object.prototype.isAdmin becomes true globally.
</script>Now, every object in the application has isAdmin: true unless specifically overridden. If the application has logic like if (user.isAdmin), and user is a plain object derived from elsewhere, the attacker has just bypassed authorization.
This isn't a complex buffer overflow where you need to recompile binaries. This is JavaScript. The fix is strictly logical.
Primary Mitigation: Update devalue to version 5.6.3 immediately. This version includes the fail-fast check that throws an error upon seeing __proto__.
Defense in Depth: Even with the patch, you should ask yourself why you are accepting objects with __proto__ keys from users. Consider sanitizing input at the API gateway level. If JSON.parse is your ingestion point, you can use a reviver function to nuke dangerous keys before they even reach your business logic.
JSON.parse(userInput, (key, value) => {
if (key === '__proto__') return undefined;
return value;
});Do not rely on devalue to be your firewall. It is a serializer, not a WAF.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
devalue Rich-Harris / sveltejs | < 5.6.3 | 5.6.3 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-1321 |
| Attack Vector | Network (Input -> Serialization -> Client Exec) |
| CVSS | 7.5 (High) |
| Impact | Prototype Pollution |
| Affected Component | devalue npm package |
| Fix Commit | 0f04d4d678eac39ad5d7a07d1956275d7874e81c |
Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution')