From 4f4fc9bb273be729a28b552ac2e19bf32673ab54 Mon Sep 17 00:00:00 2001 From: thy Date: Wed, 19 Aug 2026 14:06:16 +0100 Subject: [PATCH 1/2] feat(httpsys): support chunked transfer-encoding http.sys de-chunks request bodies natively; read until ERROR_HANDLE_EOF instead of relying on Content-Length. Bodies of unknown length are capped by MaxInputCountLength (default 100 MB) and rejected with 413 beyond that. Decompression path now shared by both read paths. Propagate MaxInputCountLength in Clone. Refs QuBES #1333 --- Libraries/SimpleServer/dwsHTTPSysServer.pas | 48 ++++++++++++++++++++- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/Libraries/SimpleServer/dwsHTTPSysServer.pas b/Libraries/SimpleServer/dwsHTTPSysServer.pas index daddc8fe..f3d00111 100644 --- a/Libraries/SimpleServer/dwsHTTPSysServer.pas +++ b/Libraries/SimpleServer/dwsHTTPSysServer.pas @@ -366,6 +366,8 @@ implementation const cDefaultMaxInputCountLength = 10*1024*1024; // 10 MB + // QuBES patch #1333: default cumulative cap for bodies of unknown length (chunked) + cDefaultMaxChunkedInputLength = 100*1024*1024; // 100 MB var vWsaDataOnce : TWSADATA; @@ -627,6 +629,7 @@ constructor THttpApi2Server.CreateClone(From : THttpApi2Server); end; FServerEvents := From.FServerEvents; FURLRewriter := From.URLRewriter; + FMaxInputCountLength := From.FMaxInputCountLength; // QuBES patch #1333 end; // SendStaticFile @@ -1250,6 +1253,8 @@ procedure THttpApi2Server.RegisterCompress(aFunction : THttpSocketCompress); // function THttpApi2Server.GetRequestContentBody(request : PHTTP_REQUEST_V2; response : PHTTP_RESPONSE_V2; var inContent : RawByteString) : Boolean; +const + cChunkedReadBufferSize = 65536; // 64 KB per ReceiveRequestEntityBody call var inContentLength, inContentLengthRead, bytesRead : Cardinal; bufRead : PAnsiChar; @@ -1257,10 +1262,47 @@ function THttpApi2Server.GetRequestContentBody(request : PHTTP_REQUEST_V2; respo i, n : Integer; compressRec : PHttpSocketCompressRec; contentEncoding : PHTTP_KNOWN_HEADER; + transferEncoding : RawByteString; + chunked : Boolean; + inputCap : Cardinal; begin with request^.Headers.KnownHeaders[reqContentLength] do inContentLength := GetCardinal(pRawValue, pRawValue+RawValueLength); - if inContentLength<>0 then begin + // QuBES patch #1333: chunked transfer-encoding support + // http.sys de-chunks natively, the body just has to be read until ERROR_HANDLE_EOF + // without assuming a known Content-Length + with request^.Headers.KnownHeaders[reqTransferEncoding] do + SetString(transferEncoding, pRawValue, RawValueLength); + chunked := (transferEncoding<>'') and (Pos('chunked', LowerCase(String(transferEncoding)))>0); + if chunked or (inContentLength=0) then begin + // unknown body length: cumulative size cap enforced while reading + if MaxInputCountLength>0 then + inputCap := MaxInputCountLength + else inputCap := cDefaultMaxChunkedInputLength; + inContentLengthRead := 0; + repeat + SetLength(inContent, inContentLengthRead+cChunkedReadBufferSize); + bufRead := PAnsiChar(Pointer(inContent))+inContentLengthRead; + bytesRead := 0; + if Win32MajorVersion>5 then // speed optimization for Vista+ + flags := HTTP_RECEIVE_REQUEST_ENTITY_BODY_FLAG_FILL_BUFFER + else flags := 0; + errCode := HttpAPI.ReceiveRequestEntityBody(FReqQueue, request^.RequestId, flags, + bufRead, cChunkedReadBufferSize, bytesRead); + inc(inContentLengthRead, bytesRead); // ERROR_HANDLE_EOF may carry final bytes + if inContentLengthRead>inputCap then begin + SendError(request, response, 413, 'Payload Too Large'); + Exit(False); + end; + if (errCode=NO_ERROR) and (bytesRead=0) then + break; // no progress means end of body + until errCode<>NO_ERROR; + if (errCode<>ERROR_HANDLE_EOF) and (errCode<>NO_ERROR) then begin + SendError(request, response, 406, SysErrorMessage(errCode)); + Exit(False); + end; + SetLength(inContent, inContentLengthRead); + end else begin if (MaxInputCountLength>0) and (inContentLength>MaxInputCountLength) then begin // ideally SendError(412, 'Content-Length too large'); // but if we don't cancel the request, it'll eat all the @@ -1293,7 +1335,9 @@ function THttpApi2Server.GetRequestContentBody(request : PHTTP_REQUEST_V2; respo SendError(request, response, 406, SysErrorMessage(errCode)); Exit(False); end; - // attempt to decompress if necessary + end; + // attempt to decompress if necessary (QuBES patch #1333: common to both read paths) + if inContent<>'' then begin contentEncoding:=@request^.Headers.KnownHeaders[reqContentEncoding]; n:=contentEncoding.RawValueLength; if n>0 then begin From a4c506f65a27ee244aa3cc4b578b84e2cc9f2749 Mon Sep 17 00:00:00 2001 From: thy Date: Wed, 19 Aug 2026 14:48:34 +0100 Subject: [PATCH 2/2] fix(httpsys): harden chunked body handling - grow read buffer geometrically to avoid O(n^2) reallocations - cancel request after 413 so the unread body cannot flood the connection - cap decompressed size too: the wire cap only bounds compressed bytes, leaving decompression bombs unchecked - hoist flags computation out of both read loops Refs QuBES #1333 --- Libraries/SimpleServer/dwsHTTPSysServer.pas | 32 +++++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/Libraries/SimpleServer/dwsHTTPSysServer.pas b/Libraries/SimpleServer/dwsHTTPSysServer.pas index f3d00111..580e16a9 100644 --- a/Libraries/SimpleServer/dwsHTTPSysServer.pas +++ b/Libraries/SimpleServer/dwsHTTPSysServer.pas @@ -1256,7 +1256,7 @@ function THttpApi2Server.GetRequestContentBody(request : PHTTP_REQUEST_V2; respo const cChunkedReadBufferSize = 65536; // 64 KB per ReceiveRequestEntityBody call var - inContentLength, inContentLengthRead, bytesRead : Cardinal; + inContentLength, inContentLengthRead, bytesRead, capacity : Cardinal; bufRead : PAnsiChar; flags, errCode : Integer; i, n : Integer; @@ -1264,8 +1264,11 @@ function THttpApi2Server.GetRequestContentBody(request : PHTTP_REQUEST_V2; respo contentEncoding : PHTTP_KNOWN_HEADER; transferEncoding : RawByteString; chunked : Boolean; - inputCap : Cardinal; + inputCap, maxBodyLength : Cardinal; begin + if Win32MajorVersion>5 then // speed optimization for Vista+ + flags := HTTP_RECEIVE_REQUEST_ENTITY_BODY_FLAG_FILL_BUFFER + else flags := 0; with request^.Headers.KnownHeaders[reqContentLength] do inContentLength := GetCardinal(pRawValue, pRawValue+RawValueLength); // QuBES patch #1333: chunked transfer-encoding support @@ -1279,19 +1282,26 @@ function THttpApi2Server.GetRequestContentBody(request : PHTTP_REQUEST_V2; respo if MaxInputCountLength>0 then inputCap := MaxInputCountLength else inputCap := cDefaultMaxChunkedInputLength; + maxBodyLength := inputCap; inContentLengthRead := 0; + capacity := cChunkedReadBufferSize; + SetLength(inContent, capacity); repeat - SetLength(inContent, inContentLengthRead+cChunkedReadBufferSize); + // grow geometrically to avoid O(n^2) reallocations on large bodies + if capacity-inContentLengthRead < cChunkedReadBufferSize then begin + capacity := capacity*2; + SetLength(inContent, capacity); + end; bufRead := PAnsiChar(Pointer(inContent))+inContentLengthRead; bytesRead := 0; - if Win32MajorVersion>5 then // speed optimization for Vista+ - flags := HTTP_RECEIVE_REQUEST_ENTITY_BODY_FLAG_FILL_BUFFER - else flags := 0; errCode := HttpAPI.ReceiveRequestEntityBody(FReqQueue, request^.RequestId, flags, bufRead, cChunkedReadBufferSize, bytesRead); inc(inContentLengthRead, bytesRead); // ERROR_HANDLE_EOF may carry final bytes if inContentLengthRead>inputCap then begin + // send the 413 first so the client gets a clean status, then cancel to stop + // the remaining (unread) body from flooding the connection SendError(request, response, 413, 'Payload Too Large'); + HttpAPI.CancelHttpRequest(FReqQueue, request^.RequestId, nil); Exit(False); end; if (errCode=NO_ERROR) and (bytesRead=0) then @@ -1303,6 +1313,7 @@ function THttpApi2Server.GetRequestContentBody(request : PHTTP_REQUEST_V2; respo end; SetLength(inContent, inContentLengthRead); end else begin + maxBodyLength := MaxInputCountLength; // 0 = historical unbounded behavior if (MaxInputCountLength>0) and (inContentLength>MaxInputCountLength) then begin // ideally SendError(412, 'Content-Length too large'); // but if we don't cancel the request, it'll eat all the @@ -1315,9 +1326,6 @@ function THttpApi2Server.GetRequestContentBody(request : PHTTP_REQUEST_V2; respo inContentLengthRead := 0; repeat bytesRead := 0; - if Win32MajorVersion>5 then // speed optimization for Vista+ - flags := HTTP_RECEIVE_REQUEST_ENTITY_BODY_FLAG_FILL_BUFFER - else flags := 0; errCode := HttpAPI.ReceiveRequestEntityBody(FReqQueue, request^.RequestId, flags, bufRead, inContentLength-inContentLengthRead, bytesRead); inc(inContentLengthRead, BytesRead); @@ -1346,6 +1354,12 @@ function THttpApi2Server.GetRequestContentBody(request : PHTTP_REQUEST_V2; respo if (compressRec.NameLength=n) and CompareMem(Pointer(compressRec.Name), contentEncoding.pRawValue, n) then begin compressRec.Func(inContent, false); // uncompress + // QuBES patch #1333: guard against decompression bombs -- the cap above + // only bounds the compressed bytes read off the wire, not the output + if (maxBodyLength>0) and (Cardinal(Length(inContent))>maxBodyLength) then begin + SendError(request, response, 413, 'Payload Too Large'); + Exit(False); + end; break; end; end;