Type confusion in 'Bundle::get'
Summary
Bundle is the per-request key/value store that BUNDLE(...) endpoints read from. Bundle::get checks the stored value's type before handing it back, but the check asks whether the requested type extends the stored type — the downcast condition. An Object<Base> put into the bundle comes back out as Object<Derived> with no exception.
Details
src/oatpp/data/Bundle.hpp:63-78:
71 if(!WrapperType::Class::getType()->extends(it->second.getValueType())) {
72 throw std::runtime_error("[oatpp::data::Bundle::get()]: Error. Type mismatch for key '" + *key +
73 "'. Stored '" +
74 std::string(it->second.getValueType()->classId.name) + ...WrapperType is the type the caller asked for and it->second is what is stored. Line 71 asks whether the requested type extends the stored one, so requesting a subclass of what is stored passes.
This matters separately from the Type.hpp cases because Bundle::get does not route through checkType; it reimplements the test. Correcting checkType alone leaves this path broken.
extends follows the parent chain, which DTO_INIT(Derived, Base) populates, so the relation being violated is C++ inheritance and not an ad-hoc tag scheme.
What the caller receives is a wrapper tagged Derived whose pointer owns a Base. ObjectToTreeMapper::mapObject then takes Derived's property list from the tag (ObjectToTreeMapper.cpp:259) and reads each field through it (:272), running off the end of the Base allocation.
Bundle is reached from ordinary request handling: ENDPOINT_INTERCEPTOR handlers commonly put values that the endpoint later gets, and the two sides are usually in different files.
Fix:
71 if(!it->second.getValueType()->extends(WrapperType::Class::getType())) {Better still, have Bundle::get call the (corrected) checkType instead of keeping a fourth copy of the comparison.
PoC
poc_Bundle_get.cpp:
class Base : public oatpp::DTO {
DTO_INIT(Base, DTO)
DTO_FIELD(String, a);
};
class Derived : public Base {
DTO_INIT(Derived, Base)
DTO_FIELD(String, f0); /* ... f1 .. f9 ... */
};
oatpp::data::Bundle bundle;
bundle.put("user", Base::createShared()); // stored: Object<Base>
auto d = bundle.get<oatpp::Object<Derived>>("user"); // Bundle.hpp:71 accepts the downcast
oatpp::json::ObjectMapper mapper;
auto out = mapper.writeToString(d); // <-- ASAN: heap-buffer-overflow READBuild and run:
clang++-22 -fsanitize=address,undefined,vptr -fsanitize-recover=all \
-fno-omit-frame-pointer -frtti -g -O1 -std=c++17 -I<oatpp>/src \
poc_Bundle_get.cpp liboatpp.a -lpthread -o poc_Bundle_get
ASAN_OPTIONS=halt_on_error=0 UBSAN_OPTIONS=halt_on_error=0 ./poc_Bundle_getSame build note as the other two: liboatpp.a with matching sanitizer flags, and -fsanitize-recover=all so the unrelated DTO property-offset defect (codegen/dto/base_define.hpp:67) does not abort first.
ASAN output:
==3009018==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7bed7ffe0118 ...
READ of size 8 at 0x7bed7ffe0118 thread T0
[...]
#2 in oatpp::data::type::ObjectWrapper<void, __class::Void>::getPtr() const src/oatpp/data/type/Type.hpp
#3 in oatpp::data::type::Void::Void(Void const&) src/oatpp/data/type/Type.hpp
#4 in oatpp::data::type::BaseObject::get(long) const src/oatpp/data/type/Object.cpp:39:10
#5 in oatpp::data::type::BaseObject::Property::get(BaseObject*) const src/oatpp/data/type/Object.cpp
#6 in oatpp::data::mapping::ObjectToTreeMapper::mapObject(...) src/oatpp/data/mapping/ObjectToTreeMapper.cpp
#7 in oatpp::data::mapping::ObjectToTreeMapper::map(...) src/oatpp/data/mapping/ObjectToTreeMapper.cpp
[...]
0x7bed7ffe0118 is located 0 bytes after 56-byte regionImpact
OOB read past the stored object, sized by the field count of the DTO the caller asked for. Reachability is application level. some code in the process has to put one type and get another.
Source: oatpp/oatpp