Skip to content

CBOR: nested indefinite-length strings/byte strings are accepted (RFC 8949 3.2.3 requires definite-length chunks) #5317

Description

@nlohmann

Description

RFC 8949 §3.2.3 requires that the chunks of an indefinite-length string be definite-length strings of the same major type:

Each chunk of an indefinite-length string is a definite-length string of the same major type.

get_cbor_string() and get_cbor_binary() read each chunk by recursing into themselves (binary_reader.hpp:1025-1037 and 1125-1137), and the recursive call re-enters the full switch including the 0x7F/0x5F case. So a chunk may itself be an indefinite-length string, to arbitrary depth, and the library accepts it.

The cross-major-type rule is already enforced correctly — a byte-string chunk inside a text string is rejected, and vice versa. It is only the "definite-length" half of the rule that is missing.

Secondary effect: because the nesting is unbounded, 0x7F repeated ~50 000 times segfaults. That aspect overlaps with #5104 / #5293, but a depth guard alone would only turn the crash into a clean parse_error — the two-level case below would still be accepted rather than rejected. The conformance fix is independent and cheaper: reject 0x7F/0x5F when already inside an indefinite-length string, which also removes this particular recursion path entirely.

Reproduction steps

Parse a two-level indefinite-length text string and byte string.

Expected vs. actual results

Expected: parse_error.113, same as any other invalid chunk head.

Actual:

input meaning result
7F 7F 61 61 FF FF indefinite tstr containing an indefinite tstr accepted, yields "a"
5F 5F 41 61 FF FF indefinite bstr containing an indefinite bstr accepted, yields h'61'
7F 41 61 FF bstr chunk inside a tstr rejected (correct)
5F 61 61 FF tstr chunk inside a bstr rejected (correct)

Minimal code example

#include <nlohmann/json.hpp>
#include <iostream>
using json = nlohmann::json;

int main()
{
    // 0x7F ( 0x7F ( "a" ) )  -- a chunk that is itself indefinite-length
    const std::vector<std::uint8_t> v{0x7F, 0x7F, 0x61, 0x61, 0xFF, 0xFF};
    std::cout << json::from_cbor(v) << "\n";   // prints "a"; should throw

    // unbounded nesting -> stack exhaustion (cf. #5104)
    std::vector<std::uint8_t> deep(50000, 0x7F);
    deep.insert(deep.end(), 50000, 0xFF);
    (void)json::from_cbor(deep);               // segfault
}

Error messages

None — the malformed input is accepted. (The deep case terminates with SIGSEGV.)

Compiler and operating system

g++ 13.3.0, Ubuntu 24.04, -std=c++11

Library version

develop @ 8ec98e2

Would the fix be breaking?

No API/ABI change, and the behavior change is confined to input that is malformed per the spec.

  • What changes: byte sequences that RFC 8949 forbids and that other CBOR implementations reject would start throwing parse_error.113 instead of being accepted. There is no valid encoder that produces them, so no legitimate document is affected.
  • Round-trip safety: the library never emits indefinite-length strings, so nothing it writes can trip the new check.
  • Risk: the only realistic breakage is a user whose non-conforming producer happens to emit nested chunks and who relies on this library's leniency. That seems remote, and tightening it matches how the reader already handles the cross-major-type half of the same rule.
  • Docs/tests: the "Deserialization" section of docs/mkdocs/docs/features/binary_formats/cbor.md doesn't currently say anything about chunk structure; a sentence could be added. I did not find an existing unit test that covers nested chunks, so nothing in unit-cbor.cpp should need to be relaxed.

Fixing this also shrinks the surface of #5104: with nesting rejected, indefinite-length strings stop being a recursion vector at all, and the depth guard in #5293 is left to handle the genuinely recursive cases (nested arrays/maps and chained tags).

Metadata

Metadata

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions