devalue vulnerable to denial of service due to memory exhaustion in devalue.parse
Description
Svelte devalue is a JavaScript library that serializes values into strings when JSON.stringify isn't sufficient for the job. From 5.3.0 to 5.6.1, certain inputs can cause devalue.parse to consume excessive CPU time and/or memory, potentially leading to denial of service in systems that parse input from untrusted sources. This affects applications using devalue.parse on externally-supplied data. The root cause is the typed array hydration expecting an ArrayBuffer as input, but not checking the assumption before creating the typed array. This vulnerability is fixed in 5.6.2.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
devaluenpm | >= 5.3.0, < 5.6.2 | 5.6.2 |
Affected products
1Patches
211755849fa06Merge commit from fork
4 files changed · +61 −3
.changeset/slow-banks-love.md+5 −0 added@@ -0,0 +1,5 @@ +--- +"devalue": patch +--- + +fix: validate input for `ArrayBuffer` parsing
.changeset/tender-cities-check.md+5 −0 added@@ -0,0 +1,5 @@ +--- +"devalue": patch +--- + +fix: more helpful errors for inputs causing stack overflows
src/parse.js+28 −1 modified@@ -33,6 +33,13 @@ export function unflatten(parsed, revivers) { const hydrated = Array(values.length); + /** + * A set of values currently being hydrated with custom revivers, + * used to detect invalid cyclical dependencies + * @type {Set<number> | null} + */ + let hydrating = null; + /** * @param {number} index * @returns {any} @@ -70,7 +77,18 @@ export function unflatten(parsed, revivers) { // so we need to munge it into the format expected by a custom reviver i = values.push(value[1]) - 1; } - return (hydrated[index] = reviver(hydrate(i))); + + hydrating ??= new Set(); + + if (hydrating.has(i)) { + throw new Error('Invalid circular reference'); + } + + hydrating.add(i); + hydrated[index] = reviver(hydrate(i)); + hydrating.delete(i); + + return hydrated[index]; } switch (type) { @@ -125,6 +143,12 @@ export function unflatten(parsed, revivers) { case 'Float64Array': case 'BigInt64Array': case 'BigUint64Array': { + if (values[value[1]][0] !== 'ArrayBuffer') { + // without this, if we receive malformed input we could + // end up trying to hydrate in a circle + throw new Error('Invalid data'); + } + const TypedArrayConstructor = globalThis[type]; const buffer = hydrate(value[1]); if (!(buffer instanceof ArrayBuffer)) { @@ -142,6 +166,9 @@ export function unflatten(parsed, revivers) { case 'ArrayBuffer': { const base64 = value[1]; + if (typeof base64 !== 'string') { + throw new Error('Invalid ArrayBuffer encoding'); + } const arraybuffer = decode64(base64); hydrated[index] = arraybuffer; break;
test/test.js+23 −2 modified@@ -745,6 +745,11 @@ const invalid = [ json: '[["Int8Array", 1], { "length": 2 }, 1000000000]', message: 'Invalid input, expected ArrayBuffer but got object' }, + { + name: 'ArrayBuffer with non-string value', + json: '[["ArrayBuffer", { "length": 100 }]]', + message: 'Invalid ArrayBuffer encoding' + }, { name: 'empty string', json: '', @@ -802,13 +807,29 @@ const invalid = [ name: 'bad index', json: '[{"0":1,"toString":"push"},"hello"]', message: 'Invalid input' + }, + { + name: 'TypedArray self-reference', + json: '[["Uint8Array", 0]]', + message: 'Invalid data' + }, + { + name: 'custom reviver self-reference', + json: '[["Custom", 0]]', + revivers: { Custom: (v) => v }, + message: 'Invalid circular reference' + }, + { + name: 'mutual TypedArray reference', + json: '[["Uint8Array", 1], ["Uint8Array", 0]]', + message: 'Invalid data' } ]; -for (const { name, json, message } of invalid) { +for (const { name, json, message, revivers } of invalid) { uvu.test(`parse error: ${name}`, () => { assert.throws( - () => parse(json), + () => parse(json, revivers), (error) => { const match = error.message === message; if (!match) {
e46afa64dd2bMerge commit from fork
3 files changed · +15 −1
.changeset/swift-planes-fry.md+5 −0 added@@ -0,0 +1,5 @@ +--- +"devalue": patch +--- + +fix: validate input for typed arrays
src/parse.js+5 −1 modified@@ -126,7 +126,11 @@ export function unflatten(parsed, revivers) { case 'BigInt64Array': case 'BigUint64Array': { const TypedArrayConstructor = globalThis[type]; - const typedArray = new TypedArrayConstructor(hydrate(value[1])); + const buffer = hydrate(value[1]); + if (!(buffer instanceof ArrayBuffer)) { + throw new Error(`Invalid input, expected ArrayBuffer but got ${typeof buffer}`); + } + const typedArray = new TypedArrayConstructor(buffer); hydrated[index] = value[2] !== undefined
test/test.js+5 −0 modified@@ -740,6 +740,11 @@ for (const [name, tests] of Object.entries(fixtures)) { } const invalid = [ + { + name: 'typed array with non-ArrayBuffer input', + json: '[["Int8Array", 1], { "length": 2 }, 1000000000]', + message: 'Invalid input, expected ArrayBuffer but got object' + }, { name: 'empty string', json: '',
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-vw5p-8cq8-m7mvghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-22774ghsaADVISORY
- github.com/sveltejs/devalue/commit/11755849fa0634ae294a15ec0aef2f43efcad7c4ghsaWEB
- github.com/sveltejs/devalue/commit/e46afa64dd2b25aa35fb905ba5d20cea63aabbf7ghsax_refsource_MISCWEB
- github.com/sveltejs/devalue/releases/tag/v5.6.2ghsax_refsource_MISCWEB
- github.com/sveltejs/devalue/security/advisories/GHSA-vw5p-8cq8-m7mvghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.