#4: BodySizeLimitInterceptor — fail-closed on missing/malformed Content-Length

Body-bearing methods (POST/PUT/PATCH) now reject:
- missing Content-Length → 411
- malformed Content-Length → 400
- non-identity Transfer-Encoding (chunked, etc.) → 411
- declared length > maxBytes → 413 (unchanged)

GET/HEAD/DELETE/OPTIONS/TRACE pass through unchanged. Consumers needing
the legacy fail-open behaviour pass `requireContentLength = false`.

Bump to 0.3.3 (behaviour tightening — consumers on default ctor see new
411/400 responses on requests that previously sailed through).

Closes #4

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Uwe Schuster 2026-04-25 21:36:38 +02:00
parent abf6153439
commit 950012d946
4 changed files with 107 additions and 20 deletions

View file

@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.14)
project(oatpp-authkit VERSION 0.3.2 LANGUAGES CXX)
project(oatpp-authkit VERSION 0.3.4 LANGUAGES CXX)
# Header-only interface library — no compilation, just an include path and
# a CMake config package so consumers do:

View file

@ -7,38 +7,96 @@
namespace oatpp_authkit {
/**
* @brief Request interceptor that rejects requests exceeding a body size limit.
* @brief Request interceptor that rejects oversized or under-declared request bodies.
*
* Checks the Content-Length header and returns HTTP 413 (Payload Too Large)
* if the declared body size exceeds the configured maximum.
* Behaviour for body-bearing methods (`POST`, `PUT`, `PATCH`):
* - missing `Content-Length` `411 Length Required` (audit #4: closes
* chunked-transfer / HTTP/2 bypass that previously sailed through silently)
* - malformed `Content-Length` `400 Bad Request`
* - `Transfer-Encoding: chunked` (or any non-identity encoding) `411`
* (we cannot enforce a cap without buffering an unbounded stream; reject
* by default rather than fall through to oatpp's much higher ceiling)
* - declared length above `maxBytes` `413 Payload Too Large`
*
* Methods that don't carry a body (`GET`, `HEAD`, `DELETE`, `OPTIONS`, `TRACE`)
* pass through untouched `Content-Length` absence is normal there.
*
* Consumers that genuinely need to accept missing/chunked bodies on body-
* bearing methods can construct with `requireContentLength = false` to revert
* to the legacy fail-open behaviour.
*/
class BodySizeLimitInterceptor : public oatpp::web::server::interceptor::RequestInterceptor {
private:
size_t m_maxBytes;
bool m_requireContentLength;
static bool methodCarriesBody(const oatpp::String& method) {
if (!method) return false;
const std::string m = *method;
return m == "POST" || m == "PUT" || m == "PATCH";
}
static std::shared_ptr<OutgoingResponse> jsonResponse(int code, const char* phrase, const char* body) {
auto r = oatpp::web::protocol::http::outgoing::ResponseFactory::createResponse(
oatpp::web::protocol::http::Status(code, phrase), body);
r->putHeader("Content-Type", "application/json");
return r;
}
public:
/**
* @param maxBytes Maximum allowed request body size in bytes.
* @param requireContentLength When `true` (default), body-bearing methods
* must declare a parseable `Content-Length`;
* missing/malformed/chunked reject. Set
* `false` for the legacy lax behaviour.
*/
explicit BodySizeLimitInterceptor(size_t maxBytes) : m_maxBytes(maxBytes) {}
explicit BodySizeLimitInterceptor(size_t maxBytes, bool requireContentLength = true)
: m_maxBytes(maxBytes), m_requireContentLength(requireContentLength) {}
std::shared_ptr<OutgoingResponse> intercept(const std::shared_ptr<IncomingRequest>& request) override {
const auto& line = request->getStartingLine();
if (!methodCarriesBody(line.method.toString())) {
return nullptr;
}
auto transferEncoding = request->getHeader("Transfer-Encoding");
if (m_requireContentLength && transferEncoding && !transferEncoding->empty()) {
std::string te = *transferEncoding;
for (auto& c : te) c = std::tolower(static_cast<unsigned char>(c));
if (te.find("identity") == std::string::npos) {
return jsonResponse(411, "Length Required",
"{\"status\":\"Length Required\"}");
}
}
auto contentLength = request->getHeader("Content-Length");
if (contentLength && !contentLength->empty()) {
if (!contentLength || contentLength->empty()) {
if (m_requireContentLength) {
return jsonResponse(411, "Length Required",
"{\"status\":\"Length Required\"}");
}
return nullptr;
}
size_t len = 0;
try {
size_t len = std::stoull(std::string(*contentLength));
if (len > m_maxBytes) {
auto response = oatpp::web::protocol::http::outgoing::ResponseFactory::createResponse(
oatpp::web::protocol::http::Status(413, "Payload Too Large"),
"{\"status\":\"Payload Too Large\"}");
response->putHeader("Content-Type", "application/json");
return response;
}
size_t pos = 0;
len = std::stoull(std::string(*contentLength), &pos);
if (pos != contentLength->size()) throw std::invalid_argument("trailing");
} catch (...) {
// Malformed Content-Length — let it through, Oat++ will handle it
if (m_requireContentLength) {
return jsonResponse(400, "Bad Request",
"{\"status\":\"Bad Request\"}");
}
return nullptr;
}
return nullptr; // pass through
if (len > m_maxBytes) {
return jsonResponse(413, "Payload Too Large",
"{\"status\":\"Payload Too Large\"}");
}
return nullptr;
}
};

View file

@ -9,3 +9,7 @@ find_package(oatpp REQUIRED)
add_executable(test_negotiation test_negotiation.cpp)
target_link_libraries(test_negotiation PRIVATE oatpp::authkit oatpp::oatpp)
add_test(NAME negotiation COMMAND test_negotiation)
add_executable(test_body_size_limit test_body_size_limit.cpp)
target_link_libraries(test_body_size_limit PRIVATE oatpp::authkit oatpp::oatpp)
add_test(NAME body_size_limit COMMAND test_body_size_limit)

View file

@ -0,0 +1,25 @@
// Smoke test for BodySizeLimitInterceptor — confirms the header compiles
// in a consumer translation unit and the constructor surface matches the
// documented API. Behavioural tests against real IncomingRequest objects
// would need a full oatpp request fixture; pinning the API surface here is
// enough to catch the kinds of breakage this header is at risk of.
#include "oatpp-authkit/interceptor/BodySizeLimitInterceptor.hpp"
#include <cstdio>
#include <memory>
int main() {
using oatpp_authkit::BodySizeLimitInterceptor;
// Default: fail-closed.
auto strict = std::make_shared<BodySizeLimitInterceptor>(1024);
(void)strict;
// Opt-out: legacy fail-open behaviour.
auto lax = std::make_shared<BodySizeLimitInterceptor>(1024, /*requireContentLength=*/false);
(void)lax;
std::printf("BodySizeLimitInterceptor API ok\n");
return 0;
}