Skip to content

PntsToGltfConverter.cpp OOB read #1425

Description

@Ssh1y

1. Vulnerability Description

The heap-buffer-overflow READ described in the report is reproducible on the exact commit (004a56a) cited. A 28-byte malicious .pnts file triggers an out-of-bounds read in PntsToGltfConverter.cpp, detected by AddressSanitizer as a heap-buffer-overflow of size 1 at the byte immediately following the 28-byte allocation.

2. Vulnerability Analysis

parsePntsHeader (line 84–114) performs only one size check on the incoming tile buffer:

// PntsToGltfConverter.cpp:108
if (static_cast<uint32_t>(pntsBinary.size()) < pHeader->byteLength) {
  result.errors.emplaceError(
      "The PNTS is invalid because the total data available is less than the "
      "size specified in its header.");
  return;
}

This check verifies that the outer buffer is at least byteLength bytes, but it does not verify that the four declared section lengths actually fit inside byteLength:

headerLength + featureTableJsonByteLength
            + featureTableBinaryByteLength
            + batchTableJsonByteLength
            + batchTableBinaryByteLength   <=   byteLength     <-- NEVER CHECKED

Consequently, when convertPntsContentToGltf (line 1531) builds the feature table JSON span:

// PntsToGltfConverter.cpp:1540-1541
const std::span<const std::byte> featureTableJsonData =
    pntsBinary.subspan(headerLength, header.featureTableJsonByteLength);

std::span::subspan performs no bounds check in release mode. If headerLength + featureTableJsonByteLength > pntsBinary.size(), the resulting span points past the end of the allocation.

That span is then handed to parseFeatureTableJson (line 556), which calls rapidjson::Document::Parse(ptr, len). RapidJSON's MemoryStream::Peek() reads the first byte at ptr — which is now an out-of-bounds heap address — triggering the heap-buffer-overflow READ.

A second, structurally identical vulnerable path exists for the batch table JSON at lines 1553–1559 (batchTableStart + subspan).

3. Vulnerability Reproduction

poc_pnts_oob.zip

Reproduction command:

g++ -std=c++20 -O1 -g -fsanitize=address,undefined -fno-omit-frame-pointer \
    -I/home/work/rapidjson/include \
    /home/work/scripts/poc_pnts_oob.cpp -o /tmp/poc && \
ASAN_OPTIONS=detect_leaks=0 /tmp/poc /home/download/poc_pnts_oob.pnts

4、Additional Vulnerable Path

The same missing-check pattern affects the batch table JSON path at lines 1553–1559:

// PntsToGltfConverter.cpp:1553-1559
const int64_t batchTableStart = headerLength +
                                header.featureTableJsonByteLength +
                                header.featureTableBinaryByteLength;
rapidjson::Document batchTableJson;
if (header.batchTableJsonByteLength > 0) {
  const std::span<const std::byte> batchTableJsonData = pntsBinary.subspan(
      static_cast<size_t>(batchTableStart),
      header.batchTableJsonByteLength);   // <-- also unchecked

A PNTS with byteLength=28, ftJson=1, ftBin=1, btJson=1 (file poc_pnts_oob_batchtable.pnts) reaches this path. In the probe it crashes at the feature-table subspan first (which is encountered earlier in the same function), but in the real library the same class of OOB read applies to the batch table as well. A proper fix must cover both paths.

5、Recommended Fix

Add a single bounds check in parsePntsHeader, immediately after the existing byteLength check (after line 113):

const uint64_t headerLength64 = sizeof(PntsHeader);
const uint64_t sectionsEnd =
    headerLength64 +
    header.featureTableJsonByteLength +
    header.featureTableBinaryByteLength +
    header.batchTableJsonByteLength +
    header.batchTableBinaryByteLength;

if (sectionsEnd > header.byteLength) {
  result.errors.emplaceError(
      "The PNTS is invalid because the feature/batch table sections extend "
      "past the byteLength declared in the header.");
  return;
}

Using uint64_t for the intermediate sum avoids uint32_t overflow in the addition. This single check protects both the feature-table and batch-table subspan calls downstream.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinggood first issueGood for newcomers

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions