Skip to content

OOB reads on truncated CBOR headers + unbounded recursion via nested containers #7

Description

@ByamB4

Summary

Three memory safety vulnerabilities were found via manual source code audit of cb0r.c.

Bug 1: Out-of-Bounds Read on Multi-Byte CBOR Headers (CWE-125) — High

Location: cb0r(), multiple locations (lines 73-98, 101-114, 128-138, and 186-198)

uint8_t *end = start + 1;
if(end > stop) {           // Only checks first byte is in bounds
    if(result) result->type = CB0R_ERR;
    return stop;
}
// ...
goto *go[*start];          // Dispatch based on type byte

l_int8:
    end += 4;              // No bounds check for start[1..8]
l_int4:
    end += 2;
l_int2:
    end += 1;
l_int1:
    end += 1;
l_int:
    goto l_finish;

Issue: The parser only checks that the first byte (start[0]) is within the [start, stop) range. For multi-byte headers (int with 1/2/4/8 byte additional, byte/string with 1/2/4 byte length, array/map with 1/2/4 byte count, tag with 1/2/4/8 byte value), the additional bytes are accessed directly via start[N] without any bounds check.

Affected code paths:

  • l_int1/2/4/8 (lines 73-82): end is advanced past the header, but bytes are read at lines 186-198 via start[size - 7] through start[size] without checking bounds
  • l_byte1/2/4 (lines 85-95): start[1], start[2], start[size] accessed directly at lines 87-94 to compute length
  • l_array1/2/4 (lines 101-111): start[1], start[2], start[size] accessed at lines 103-110 to compute count
  • l_tag1/2/4/8 (lines 128-138): Tag value bytes accessed at lines 186-198

Concrete example: Input of 1 byte: \x1b (8-byte unsigned integer). The parser checks start + 1 <= stop → passes. Then dispatches to l_int8. At the result extraction (lines 186-198), it reads start[1] through start[8]8 bytes past the 1-byte input buffer.

Impact: Information disclosure via OOB read, or crash if the buffer is at a page boundary.

Bug 2: Unbounded Recursion via Nested Containers (CWE-674) — Medium

Location: cb0r(), lines 121, 138, 144

// For arrays (line 121):
end = cb0r(start+size+1, stop, count-1, NULL);
// For tags (line 138):
end = cb0r(start+size+1, stop, 0, NULL);
// For indefinite (line 144):
end = cb0r(start+1, stop, count, NULL);

Issue: Arrays, maps, and tags cause recursive calls. Nesting depth = recursion depth. With deeply nested CBOR (e.g., array of 1 containing array of 1 containing...), each level costs 2 bytes (0x81 + next level). An input of 10,000 bytes can create 5,000 levels of nesting, exceeding typical stack limits.

Trigger: Repeated bytes 0x81 (array of 1 item):

\x81\x81\x81\x81\x81\x81...\x81\x00

Each 0x81 creates one level of recursion.

Impact: Stack overflow → crash/DoS. On embedded systems (cb0r's target), stack space is typically very limited (1-8 KB).

Bug 3: Pointer Overflow in Byte String Length (CWE-190) — Low

Location: cb0r(), lines 85-94 (l_byte4)

l_byte4:
    size = 2;
    end += (uint32_t)(start[1]) << 24;
    end += (uint32_t)(start[2]) << 16;
l_byte2:
    size += 1;
    end += (uint32_t)(start[size]) << 8;
l_byte1:
    size += 1;
    end += start[size] + size;

Issue: On 32-bit platforms, the accumulated additions to end (a pointer) can overflow, causing end to wrap around. For example, a 4-byte length of 0xFFFFFFFF adds ~4GB to a pointer, wrapping it to before start.

Impact: On 32-bit: invalid pointer returned, potential use in subsequent parsing.

Proof of Concept (Bug 1)

#include <stdio.h>
#include <stdint.h>
#include "cb0r.h"

int main() {
    // Single byte: 0x1b = 8-byte unsigned int
    // Parser reads start[1] through start[8] → 8 bytes OOB
    uint8_t buf[1] = { 0x1b };
    cb0r_s result;
    cb0r_read(buf, 1, &result);
    // result.value now contains 8 bytes of OOB data
    printf("OOB value read: 0x%016llx\n", (unsigned long long)result.value);
    return 0;
}

Suggested Fixes

Bug 1:

Add bounds checking after determining header size:

l_int8:
    end += 4;
l_int4:
    end += 2;
l_int2:
    end += 1;
l_int1:
    end += 1;
    if(end > stop) goto l_eparse;   // ADD THIS CHECK
l_int:
    goto l_finish;

Apply the same pattern for l_byte1/2/4, l_array1/2/4, and l_tag1/2/4/8.

Bug 2:

Add a recursion depth parameter or limit:

#define CB0R_MAX_DEPTH 32
// Add depth parameter to cb0r(), check at entry

Found via manual source code audit. Reported responsibly — please let me know if you need any additional information.

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