CVE Patch Review

CVE-2026-45740: Root-cause Fix for protobufjs Uncontrolled Recursion

CVE-2026-45740 · GHSA-JGGG-4JG4-V7C6 · Updated 2026-05-19 Root-cause

Summary

The patch addresses the denial-of-service condition by introducing explicit depth checks across schema parsing and descriptor construction paths, and by separating general recursion limits from schema declaration nesting limits. The change is materially stronger than a localized guard because it updates multiple recursive entry points, adds a dedicated util.nestingLimit default, and includes regression tests for descriptor nesting and package path depth.

Analysis

Vulnerability

GHSA-JGGG-4JG4-V7C6, CVE-2026-45740, and the CVE record describe an uncontrolled recursion issue in protobufjs that can crash a Node.js process when parsing attacker-controlled, deeply nested schema-like input. The patch evidence shows the root problem was not a single missing guard, but inconsistent depth enforcement across recursive schema parsing, descriptor conversion, namespace traversal, and text-format handling.

Before the fix, protobufjs relied on a generic util.checkDepth helper tied to util.recursionLimit, but several schema-declaration paths needed a stricter semantic bound. In particular, recursive descriptor loading and parser nesting could continue until stack exhaustion or excessive CPU/memory consumption. The commit introduces a dedicated schema declaration limit, util.nestingLimit, defaulting to 32, while retaining util.recursionLimit at 100 for broader recursive operations, matching the patch notes in the official commit 9050289ad214ea351d3b030cbc74385e81e02d79.

// src/util/minimal.js
util.nestingLimit = 32; // protoc: MaxMessageDeclarationNestingDepth
util.recursionLimit = 100; // protoc: CodedInputStream::default_recursion_limit_

Patch

The patch removes the old shared util.checkDepth abstraction and replaces it with explicit, context-specific checks at recursive call sites. This is important because the vulnerable behavior involved multiple recursion domains with different safety thresholds.

Key changes visible in the official patch commit:

  • Descriptor recursion is bounded explicitly. Type.fromDescriptor now accepts a depth parameter, initializes it to 0, rejects values above $protobuf.util.nestingLimit, and increments depth on recursive descent into nestedType.
  • Parser/schema declaration paths distinguish nesting from general recursion. In src/parse.js and src/type.js, schema declaration depth is checked against util.nestingLimit, while other recursive traversals still use util.recursionLimit.
  • Namespace and path traversal now enforce bounds inline. src/namespace.js, src/root.js, and src/service.js now normalize undefined depth to 0 and throw Error("max depth exceeded") directly when limits are exceeded.
  • Text-format code is simplified to use the global recursion limit directly. The patch removes the separate configurable textformat.recursionLimit property and uses util.recursionLimit consistently in parsing and formatting routines.
  • Regression tests were added. tests/api_descriptor.js verifies both acceptance at the configured limit and rejection beyond it for nested descriptor messages and package path depth.
// ext/descriptor.js
Type.fromDescriptor = function fromDescriptor(descriptor, edition, nested, depth) {
    if (depth === undefined)
        depth = 0;
    if (depth > $protobuf.util.nestingLimit)
        throw Error("max depth exceeded");
    type.add(Type.fromDescriptor(descriptor.nestedType[i], edition, true, depth + 1));
};

Review

Pros

  • Addresses the actual failure mode across multiple entry points. The fix is not limited to one parser function; it updates descriptor import, schema parsing, namespace traversal, root/service/type handling, and text-format recursion. That breadth is consistent with a root-cause-oriented remediation.
  • Introduces a semantically correct limit model. Splitting nestingLimit from recursionLimit is a strong design improvement. Deep schema declaration nesting and general recursive processing are related but not identical concerns, and the patch reflects that distinction.
  • Uses fail-fast checks before recursive descent. The new guards execute before deeper recursion, reducing the chance of stack growth before rejection.
  • Regression coverage is relevant. The added tests exercise both positive and negative cases and explicitly validate the new separation between nesting and recursion limits.
  • Defaults are source-grounded. The comments indicate alignment with protoc-style limits, which improves interoperability expectations and makes the chosen thresholds less arbitrary.

Cons

  • Potential compatibility impact. Removing the dedicated textformat.recursionLimit API from ext/textformat.js and its declaration file may break consumers that tuned text-format recursion independently of global utility settings.
  • Limit semantics remain globally mutable. Because util.nestingLimit and util.recursionLimit are writable globals, applications or dependencies can still weaken protections at runtime.
  • Depth checks use > rather than >=. This is likely intentional and consistent with the tests, but it means the effective maximum accepted depth equals the configured limit rather than limit minus one. Engineers should verify that all call sites interpret depth consistently.
  • No evidence here of iterative parsing refactors. The patch mitigates denial of service by bounding recursion, but it does not eliminate recursive implementation patterns themselves.

Verdict

Root-cause.

The patch materially fixes the underlying issue by introducing explicit depth enforcement where recursion actually occurs and by separating schema nesting limits from broader recursion limits. The changes in ext/descriptor.js, src/parse.js, src/type.js, and related modules show a systematic correction rather than a narrow input filter. The added tests in tests/api_descriptor.js further support that the maintainers validated both vulnerable and non-vulnerable boundary conditions. The main residual concern is API compatibility and the continued use of mutable global limits, not an obvious remaining bypass in the patched paths documented by the official sources.

Sources: official patch commit, GitHub advisory, NVD, CVE record.

Sources