<feed xmlns='http://www.w3.org/2005/Atom'>
<title>uhttpd/tests, branch master</title>
<subtitle>Tiny HTTP server</subtitle>
<id>https://git.openwrt.org/project/uhttpd/atom?h=master</id>
<link rel='self' href='https://git.openwrt.org/project/uhttpd/atom?h=master'/>
<link rel='alternate' type='text/html' href='https://git.openwrt.org/project/uhttpd/'/>
<updated>2026-11-05T21:40:21Z</updated>
<entry>
<title>ubus: keep ubus list response encoding within encodable bounds</title>
<updated>2026-11-05T21:40:21Z</updated>
<author>
<name>Jo-Philipp Wich</name>
</author>
<published>2026-11-05T18:48:03Z</published>
<link rel='alternate' type='text/html' href='https://git.openwrt.org/project/uhttpd/commit/?id=93b05e0b65d91c23c5aa8fdde378df1f174ec71d'/>
<id>urn:sha1:93b05e0b65d91c23c5aa8fdde378df1f174ec71d</id>
<content type='text'>
uh_ubus_list_cb() opened one nesting level per signature method of every
object returned by a ubus lookup and closed the cookie returned by
blobmsg_open_table() without checking it:

	t = blobmsg_open_table(data-&gt;buf, blobmsg_name(sig));
	(...)
	blobmsg_close_table(data-&gt;buf, t);

Blob attribute lengths are 24 bit wide, so a blob can never grow beyond
BLOB_ATTR_LEN_MASK (16 MiB) bytes. Once the accumulated "list" result
approaches that limit, blob_new() fails and blobmsg_open_nested() returns
NULL, which libubox encodes as an invalid offset into the buffer.
blobmsg_close_table() then handed (char *)buf-&gt;buf - BLOB_COOKIE to
blob_nest_end(), which both read and wrote through that pointer,
corrupting the blob header and the heap around it.

No authentication is required to reach this code. A request such as

	{"jsonrpc":"2.0","method":"list","params":["*","*", ...]}

grew the response by the size of the whole object signature list per
repeated path; AddressSanitizer reports the resulting SEGV in
blob_nest_end() called from uh_ubus_list_cb(). The reporter shaped the
blob header corruption into unauthenticated remote command execution
running as root.

Fix the nesting bookkeeping and bound the response:

- Check the cookie returned by blobmsg_open_table() / blobmsg_open_array()
  before encoding into the nesting level and before closing it, in the
  list callback as well as in the remaining response builders.
- Track encoding failures in struct list_data, stop looking up further
  objects and discard a partial result instead of formatting it.
- Cap the accumulated list result at UH_UBUS_MAX_LIST_SIZE and answer with
  a JSON-RPC "Internal error" (-32603), or an HTTP 500 for the
  unparameterized GET list, instead of continuing to encode into a blob
  which no longer fits.
- Skip duplicate lookup paths, which resolve to the very same objects and
  used to duplicate result keys while growing the blob without a bound,
  and cap the number of paths a single request may name at
  UH_UBUS_MAX_LIST_PATHS ("Invalid parameters", -32602).
- Handle blob_buf_init(), blobmsg_add_blob() and blobmsg_format_json()
  failures in the affected code paths (blobmsg_add_* return negative
  values, not pointers). When formatting a response fails, still emit a
  JSON-RPC "Internal error" object instead of nothing, which left an empty
  body for single requests and a dangling separator within batches.
- The per-request bound does not bound a JSON-RPC batch: a 64 KiB POST
  holds well over a thousand "list" calls, each answered with up to
  UH_UBUS_MAX_LIST_SIZE, and all of it was queued into the unbounded
  write buffer whether or not the peer read it. Hold back the next batch
  element while more than UH_UBUS_MAX_PENDING_WRITE bytes are still
  queued for the peer, resume from the write callback once it caught up,
  and drop the connection if it does not drain within script_timeout.

Add tests/ubus_list_encoding.py, which runs uhttpd against a throwaway
ubusd holding synthetic objects with large signatures: the request from
the report makes the unpatched build die with SIGSEGV in blob_nest_end(),
while the patched build encodes every object once, answers oversized
requests with a bounded error and keeps serving normal requests.

Reported-by: @jro-calif
Link: https://github.com/openwrt/uhttpd/security/advisories/GHSA-788h-w5m4-j5xv
Signed-off-by: Jo-Philipp Wich &lt;jo@mein.io&gt;
</content>
</entry>
<entry>
<title>client: keep request parsing inside the bytes that are actually buffered</title>
<updated>2026-11-05T21:36:33Z</updated>
<author>
<name>Jo-Philipp Wich</name>
</author>
<published>2026-11-01T22:03:59Z</published>
<link rel='alternate' type='text/html' href='https://git.openwrt.org/project/uhttpd/commit/?id=b0aaa81a166ed7f8ed7264726e4af3994b6d0ae3'/>
<id>urn:sha1:b0aaa81a166ed7f8ed7264726e4af3994b6d0ae3</id>
<content type='text'>
The chunked request body parser located line ends with strstr(), which
scans the whole read buffer instead of only the bytes the ustream
currently holds. When the CRLF terminating a chunk arrived split over two
packets, the parser skipped two bytes that were not buffered yet and then
matched a CRLF left over from earlier data past the tail of the buffer.
Consuming up to that match made ustream_consume() abort the server, so an
unauthenticated remote client could kill uhttpd with a crafted chunked
request body.

Add find_crlf(), which searches for a CR/LF pair within the valid buffer
length only, and use it for the request line, the header lines, the chunk
size lines and the trailer lines. Also wait for a complete chunk size line
before parsing it, and skip chunk extensions as defined by RFC 9112 7.1.1
(BWS, token or quoted-string values) so that valid size lines are not
mistaken for malformed framing.

Add tests/chunked_body.py, which covers the request shapes from the
report, size lines and chunk terminators split across packets, chunk
extensions, trailer sections, malformed framing and randomized
fragmentation, asserting the received body length and that the server is
still alive.

Reported-by: Guy Godfroy &lt;guy.godfroy@gugod.fr&gt;
Signed-off-by: Jo-Philipp Wich &lt;jo@mein.io&gt;
</content>
</entry>
</feed>
