#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
950012d946
4 changed files with 107 additions and 20 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
cmake_minimum_required(VERSION 3.14)
|
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
|
# Header-only interface library — no compilation, just an include path and
|
||||||
# a CMake config package so consumers do:
|
# a CMake config package so consumers do:
|
||||||
|
|
|
||||||
|
|
@ -7,38 +7,96 @@
|
||||||
namespace oatpp_authkit {
|
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)
|
* Behaviour for body-bearing methods (`POST`, `PUT`, `PATCH`):
|
||||||
* if the declared body size exceeds the configured maximum.
|
* - 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 {
|
class BodySizeLimitInterceptor : public oatpp::web::server::interceptor::RequestInterceptor {
|
||||||
private:
|
private:
|
||||||
size_t m_maxBytes;
|
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:
|
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 {
|
std::shared_ptr<OutgoingResponse> intercept(const std::shared_ptr<IncomingRequest>& request) override {
|
||||||
auto contentLength = request->getHeader("Content-Length");
|
const auto& line = request->getStartingLine();
|
||||||
if (contentLength && !contentLength->empty()) {
|
if (!methodCarriesBody(line.method.toString())) {
|
||||||
try {
|
return nullptr;
|
||||||
size_t len = std::stoull(std::string(*contentLength));
|
}
|
||||||
if (len > m_maxBytes) {
|
|
||||||
auto response = oatpp::web::protocol::http::outgoing::ResponseFactory::createResponse(
|
auto transferEncoding = request->getHeader("Transfer-Encoding");
|
||||||
oatpp::web::protocol::http::Status(413, "Payload Too Large"),
|
if (m_requireContentLength && transferEncoding && !transferEncoding->empty()) {
|
||||||
"{\"status\":\"Payload Too Large\"}");
|
std::string te = *transferEncoding;
|
||||||
response->putHeader("Content-Type", "application/json");
|
for (auto& c : te) c = std::tolower(static_cast<unsigned char>(c));
|
||||||
return response;
|
if (te.find("identity") == std::string::npos) {
|
||||||
}
|
return jsonResponse(411, "Length Required",
|
||||||
} catch (...) {
|
"{\"status\":\"Length Required\"}");
|
||||||
// Malformed Content-Length — let it through, Oat++ will handle it
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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)
|
add_executable(test_negotiation test_negotiation.cpp)
|
||||||
target_link_libraries(test_negotiation PRIVATE oatpp::authkit oatpp::oatpp)
|
target_link_libraries(test_negotiation PRIVATE oatpp::authkit oatpp::oatpp)
|
||||||
add_test(NAME negotiation COMMAND test_negotiation)
|
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