client_poll_post_data() (pre-4f307a2): OOB read and NUL write past the read buffer when a chunk terminator is split across TCP segments

未关闭
#46 0 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

还没有人认领这个 Issue。

评估

难度
4/5
预计耗时
3-5 天
新手友好度
24/100
Issue 类型
缺陷
描述清晰度
描述清楚
活跃度
活跃
技术栈
c, linux
领域
backend, security

调研方向

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 (pin 7e64e8ba, 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 pin 373145f7.
  • 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; chunked state 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,通用步骤见我们的新手贡献指南。

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

openwrt/uhttpd 的其他 Issue

查看 openwrt/uhttpd 的全部 Issue

相似的 Issue

更多 C Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。