client_poll_post_data() (pre-4f307a2): OOB read and NUL write past the read buffer when a chunk terminator is split across TCP segments
还没有人认领这个 Issue。
评估
调研方向
Read SECURITY.md and the pre-4f307a2 implementation of client_poll_post_data() in client.c, then compare it with the fix in commit 4f307a2. The report includes an ASAN reproduction and describes the affected request conditions; confirm the out-of-bounds accesses and verify the fix against that reproduction. The issue documents a historical vulnerability, and its stated fix is already in master, so check whether any remaining work is intended.
由索引模型根据 Issue 内容生成。
描述
Context
This issue documents the memory-safety face of the chunked-body parsing rework that landed in
4f307a2 (2026-07-21, "client: verify chunk data is followed by CRLF before resyncing"). That
commit is public, the openwrt-24.10 series it would have been backported to has reached End of
Life, and no CVE or advisory currently covers the out-of-bounds accesses — so per
SECURITY.md this is filed publicly. Nothing here is currently exploitable on master or
25.12.x; the audience for this write-up is anyone shipping pre-4f307a2 uhttpd (EoL
OpenWrt releases, vendor firmware), plus the historical record.
Affected range
- Vulnerable: uhttpd commits from
a5138de(2013-01-03) up to, but not including,4f307a2
(2026-07-21). The openwrt-24.10 branch (pin7e64e8ba, shipped in 24.10.5 and later point
releases) contains the vulnerable code and, being EoL, will not receive a fix. - Fixed: master since
4f307a2; openwrt-25.12 since pin373145f7. - The defect is distinct from GHSA-p55c-rmhc-qfm5 / CVE-2026-55612 (invalid chunk-length state
reset on the error branch — no memory unsafety) and from issue #3 /893fed8(chunk-size
syntax). As far as we can tell no CVE covers the out-of-bounds accesses described here.
Root cause
Pre-4f307a2 code in client_poll_post_data():
if (!r->transfer_chunked)
break;
if (r->transfer_chunked > 1)
offset = 2;
sep = strstr(buf + offset, "\r\n");
if (!sep)
break;
*sep = 0;
errno = 0;
chunk_len = strtol(buf + offset, &sep, 16);
...
ustream_consume(cl->us, sep + 2 - buf);
r->content_length = (int)chunk_len;
When transfer_chunked > 1 (a chunk body has just been relayed and the parser expects that
body's two-byte CRLF terminator), the parse start is advanced by two bytes unconditionally.
Two things are never checked: that two bytes are actually buffered — ustream_get_read_buf()
can legitimately return a single byte here, because a TCP peer controls where segment
boundaries fall — and that those two bytes are \r\n at all. With one byte buffered, buf + 2
starts past the valid byte and past libubox's NUL terminator, and every quantity the parser
derives afterwards — the strstr() scan, the NUL written through *sep, the chunk length from
strtol(), the ustream_consume() length — comes from memory outside the connection's data.
Reproduction A: stock binary, default configuration
Official 24.10.5 MIPS binary (mips_24kc, sha256
43239e6a246c35897123c12c1ccc2f64842174b24a8545b5b93ef2cd663dba56, byte-identical to the ipk
payload) under qemu-mips-static, rootfs assembled from the official 24.10.5 ipk set, POST to
/cgi-bin/test.cgi with Transfer-Encoding: chunked and no Content-Length:
- Segment 1:
5\r\nHELLO\r(chunk body plus the first byte of its terminator). - Pause ~0.8 s (the chunk body is relayed to the CGI; exactly one terminator byte remains
buffered;chunkedstate is now "expect terminator"). - Segment 2, attacker-planned residue:
\n1f\r\n+ filler.
Result: the parser skips two bytes from a one-byte buffer, scans out of bounds, and derives
its framing from out-of-buffer bytes. The connection wedges permanently — no response bytes,
no CGI output, for 14 s+ (we stopped waiting there). The server process survives; the impact
is per-connection: parser desync and a stuck handler slot.
Control: with benign second-segment content (\n0\r\n\r\n) the scan finds no \r\n out of
bounds, the parser waits, and framing happens to resync when the remainder arrives — the
request completes normally and the CGI receives exactly the 5 announced bytes. Whether a given
split segfaults, desyncs, or self-heals depends on heap residue, not on the parser.
Prerequisites: Transfer-Encoding: chunked without Content-Length (both together → 400;
TE value other than chunked → 501), and a request dispatched to a handler that consumes the
request body (CGI, relay, ubus; static file requests never reach the chunk parser).
Reproduction B: ASAN
uhttpd 7e64e8ba + the 17 openwrt-24.10 distro patches, libubox master (a396f38), both
with -fsanitize=address -fsanitize-recover=address, x86_64. We set libubox's read-block size
to 1024 bytes (r.buffer_len) so a block boundary sits at the trigger point; with the default
4096-byte blocks the same crossing needs roughly 4 KB of preceding request data in the same
connection. First 1024 bytes = headers plus the chunk 5\r\nHELLO\r, so the terminator's CR
is the block's last byte; pause; then \n0\r\n\r\n:
==8==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x619000000ea4
READ of size 1 at 0x619000000ea4 thread T0
#0 in client_poll_post_data client.c:502 <- sep = strstr(buf + offset, "\r\n")
#1 in client_data_cb client.c:553
#2 in client_header_cb client.c:573
#3 in uh_client_read_cb client.c:601
#4 in ustream_fill_read libubox/ustream.c:358
...
0x619000000ea4 is located 3 bytes to the right of 1057-byte region
[0x619000000a80,0x619000000ea1)
allocated by thread T0 here:
#1 ustream_alloc_default libubox/ustream.c:70
#3 ustream_reserve libubox/ustream.c:317
SUMMARY: AddressSanitizer: heap-buffer-overflow client.c:502 in client_poll_post_data
Five such reports per trigger, all at the strstr() call site. The identical sequence with
4f307a2's two checks applied (len < 2 → break; validate buf[0]/buf[1] are \r\n
before skipping) produces clean responses and zero ASAN reports. (Line numbers refer to the
patched 24.10 source tree; in master's history the equivalent code is the block removed by
4f307a2.)
Mitigations for users of EoL releases shipping pre-4f307a2 uhttpd
- Do not expose CGI/ubus endpoints to untrusted networks (the chunk parser is only reachable
through body-consuming handlers; static file serving is unaffected). - Front the server with a proxy that re-chunks or buffers and validates chunk framing.
- Longer term: move to a release line whose uhttpd includes
4f307a2(master, 25.12.x since
373145f7).
We did not attempt to escalate to controlled memory corruption; whether the cross-block write
lands in a neighbouring allocation depends on libubox's allocator behaviour.
cc @jow- @champtar
- 主要语言
- C
- 星标
- 39
- 派生
- 31
- PR 合并指标
- 30 天内没有已合并 PR
环境准备
这个项目没有提供开发容器、Dockerfile 或贡献指南,环境需要你自己搭建:先看它的 README,通用步骤见我们的新手贡献指南。
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
openwrt/uhttpd 的其他 Issue
-
难度 4/5 3-5 天 新手友好度 48/100
-
难度 4/5 3-5 天 新手友好度 56/100
相似的 Issue
-
bug
难度 2/5 1-3 小时 新手友好度 78/100
FujiNetWIFI/fujinet-config#379 ·
维护者通常 1 天内回复
-
Ship /Local/Applications, the Local domain's Applications folder可能已有人在做 @pkgdemon 今天认领。 未关闭area:base enhancement
难度 1/5 1 小时以内 新手友好度 94/100
nextbsd/nextbsd-userland#612 ·
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 78/100
zephyrproject-rtos/zephyr#125543 ·
维护者通常 2 天内回复
-
good first issue needs-triage
难度 1/5 1 小时以内 新手友好度 88/100
InauguralSystems/EigenScript#2347 · 1 条评论 ·
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 78/100
colbymchenry/codegraph#3277 ·
维护者通常 1 天内回复