#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:
parent
abf6153439
commit
684d188918
4 changed files with 107 additions and 20 deletions
|
|
@ -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.3 LANGUAGES CXX)
|
||||
|
||||
# Header-only interface library — no compilation, just an include path and
|
||||
# a CMake config package so consumers do:
|
||||
|
|
|
|||
|
|
@ -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 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 {
|
||||
auto contentLength = request->getHeader("Content-Length");
|
||||
if (contentLength && !contentLength->empty()) {
|
||||
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;
|
||||
}
|
||||
} catch (...) {
|
||||
// Malformed Content-Length — let it through, Oat++ will handle it
|
||||
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\"}");
|
||||
}
|
||||
}
|
||||
return nullptr; // pass through
|
||||
|
||||
auto contentLength = request->getHeader("Content-Length");
|
||||
if (!contentLength || contentLength->empty()) {
|
||||
if (m_requireContentLength) {
|
||||
return jsonResponse(411, "Length Required",
|
||||
"{\"status\":\"Length Required\"}");
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
size_t len = 0;
|
||||
try {
|
||||
size_t pos = 0;
|
||||
len = std::stoull(std::string(*contentLength), &pos);
|
||||
if (pos != contentLength->size()) throw std::invalid_argument("trailing");
|
||||
} catch (...) {
|
||||
if (m_requireContentLength) {
|
||||
return jsonResponse(400, "Bad Request",
|
||||
"{\"status\":\"Bad Request\"}");
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (len > m_maxBytes) {
|
||||
return jsonResponse(413, "Payload Too Large",
|
||||
"{\"status\":\"Payload Too Large\"}");
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
25
test/test_body_size_limit.cpp
Normal file
25
test/test_body_size_limit.cpp
Normal 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;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue