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 <noreply@anthropic.com>
This commit is contained in:
Brad ArantandClaude Sonnet 5 committed 2026-10-10 07:39:46 +00:00
1 parent 05374bdb15
commit 2cea81c996
1 file changed
+18
+18
View File
@@ -104,6 +104,24 @@ namespace coreutils {
return Status::COMPLETE; return Status::COMPLETE;
} }
int contentLength = contentLengthHeader.asInteger(); 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"); MString contentType = getHeader("Content-Type");
if (contentType.startsWith("application/x-www-form-urlencoded") || if (contentType.startsWith("application/x-www-form-urlencoded") ||
contentType.startsWith("multipart/form-data")) { contentType.startsWith("multipart/form-data")) {