From 2cea81c99692e631519266e4869cb4dda831eed0 Mon Sep 17 00:00:00 2001 From: Brad Arant Date: Sat, 10 Oct 2026 07:39:46 +0000 Subject: [PATCH] Fix heap corruption from unvalidated Content-Length causing repeated production crashes HTTPRequest::parse() passed the parsed Content-Length straight into a bounds check and a buffer read with no validation. ZString::asInteger() accumulates digits into a plain 32-bit int with no overflow check, so a request claiming an absurd Content-Length (confirmed live: a classic SSTP vulnerability-scanner probe sending "Content-Length: 18446744073709551615") silently overflowed to -1. That negative value passed the "buffer.getLength() < contentLength" guard for any real buffer size, then got used as a length in ZString(cursor, contentLength) - reinterpreted as an enormous size_t, this read far past the real buffer and corrupted the heap, crashing the process (sometimes immediately, sometimes much later in an unrelated objects destructor) every time this scanner traffic reached it. This single bug was silently crash-looping barant.com (3x in ~30 hours) and the ClockPlanet server (3x over the same window) before systemds auto-restart each time masked the outage - confirmed via live core dump analysis (gdb bt full showing double free or corruption (out) inside MString::~MString, called from HTTPRequest::parse, with contentLength = -1 in the frame). Fixed by rejecting a negative Content-Length immediately, before it is ever used - the same throw-on-malformed-input pattern this function already uses for a bad request line and unsupported Transfer-Encoding. Verified live against the exact attack string on both barant.com and the ClockPlanet server: the connection is now cleanly rejected and the service stays up with no restart. Co-Authored-By: Claude Sonnet 5 --- HTTPRequest.cpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/HTTPRequest.cpp b/HTTPRequest.cpp index 4022878..8d758c2 100644 --- a/HTTPRequest.cpp +++ b/HTTPRequest.cpp @@ -104,6 +104,24 @@ namespace coreutils { return Status::COMPLETE; } int contentLength = contentLengthHeader.asInteger(); + // asInteger() accumulates digits into a plain 32-bit int with no + // overflow check - a Content-Length long enough to overflow it + // (confirmed live via real scanner/bot traffic: a request for + // "/sra_{...}/' claiming "Content-Length: 18446744073709551615", + // a classic SSTP vulnerability-probe pattern) silently wraps to a + // garbage value, observed as -1. That negative value then passed + // the "(int)buffer.getLength() < contentLength" check below for + // every real buffer length (nothing is ever less than -1), so + // execution fell through to "ZString(buffer.getCursor(), + // contentLength)" with a negative length - reinterpreted as a + // huge size_t, this read far past the real buffer and corrupted + // the heap, crashing (sometimes immediately, sometimes much + // later, in a wholly unrelated MString's destructor) on every + // production apiserver this scanner traffic reached. Rejecting + // a negative Content-Length here, before it is ever used, closes + // this off at the only place that actually needs to care. + if (contentLength < 0) + throw coreutils::Exception("HTTPRequest: invalid Content-Length."); MString contentType = getHeader("Content-Type"); if (contentType.startsWith("application/x-www-form-urlencoded") || contentType.startsWith("multipart/form-data")) {