<feed xmlns='http://www.w3.org/2005/Atom'>
<title>uhttpd, 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>uhttpd: cap script slots per source to prevent pool monopolisation</title>
<updated>2026-11-05T21:40:21Z</updated>
<author>
<name>Jo-Philipp Wich</name>
</author>
<published>2026-09-16T19:39:36Z</published>
<link rel='alternate' type='text/html' href='https://git.openwrt.org/project/uhttpd/commit/?id=8791d398c80c59b8231201c9b1aaa721d867de8a'/>
<id>urn:sha1:8791d398c80c59b8231201c9b1aaa721d867de8a</id>
<content type='text'>
Follow-up hardening for the unauthenticated abandoned-request DoS
(GHSA-p2hf-3hpj-hfpc). The previous commit stops an abandoned request
body from pinning a script slot, which closes the specific attack; this
commit additionally limits how many of the global max_script_requests
slots a single source address may hold at once, so that even a source
making many *legitimate* concurrent CGI/Lua/ucode requests cannot
monopolise the pool and starve every other client (e.g. the LuCI
management UI) of script execution.

Changes:
  * uhttpd.h: add int max_source_script_requests to struct config and
    a bool script flag to struct client (set while the connection holds
    a script slot, cleared when it is released).
  * client.c: add uh_source_script_count() which counts the running
    script slots (cl-&gt;script) held by clients sharing a given peer
    address.
  * file.c: mark cl-&gt;script in uh_invoke_script() and clear it in
    uh_complete_request(); in uh_invoke_handler() defer a request (to
    the existing pending queue) when its source has already reached the
    per-source cap. Deferred requests hold no slot and are not counted;
    only running slots are. When a slot frees, uh_complete_request()
    promotes the first queued request whose source is below its cap
    rather than the queue head, so queued requests of a source at its
    cap cannot be promoted past it. ubus is unaffected (it does not use a
    script slot).
  * main.c: add -W &lt;count&gt; to set the per-source cap. By default it is
    half of max_script_requests, rounded up: with the default pool of 3
    a single source (e.g. a browser or a reverse proxy) still gets 2
    concurrent scripts, while a pool of 2 or more always keeps at least
    one slot for other sources. -W 0 disables the cap and restores the
    previous global-only behaviour.

Verified with the default pool of 3: of three concurrent slow CGIs from
one source, two run in parallel and the third is deferred until one of
them completes, while a request from a different source still gets the
remaining slot immediately. With -W 0 the first source takes all three
slots and the other source has to wait. 6 concurrent fast CGIs from
one source all complete immediately (deferred ones resume as slots
free). No regression to static, normal POST/GET CGI, keep-alive, or
concurrent handling.

Signed-off-by: Jo-Philipp Wich &lt;jo@mein.io&gt;
</content>
</entry>
<entry>
<title>client: release script slot when the peer closes with an unfinished body</title>
<updated>2026-11-05T21:36:33Z</updated>
<author>
<name>Jo-Philipp Wich</name>
</author>
<published>2026-09-16T19:38:55Z</published>
<link rel='alternate' type='text/html' href='https://git.openwrt.org/project/uhttpd/commit/?id=8e0d492c1d3fbfccfc007e8dabf99767afff1d7b'/>
<id>urn:sha1:8e0d492c1d3fbfccfc007e8dabf99767afff1d7b</id>
<content type='text'>
An HTTP request that announces a body (Content-Length or Transfer-Encoding:
chunked) but closes the connection before sending it leaves the client in
CLIENT_STATE_DATA with a pending script dispatch. When the peer's FIN then
fires, uh_client_notify_state() unconditionally returned on
CLIENT_STATE_DATA, so the connection was never closed and the script slot
was never released. The connection then pinned one of the (by default) 3
script requests until script_timeout expired; three such connections
exhausted the pool and denied CGI/Lua/ucode requests (e.g. the LuCI
management UI) to every client.

Add uh_client_body_complete(), which reports whether the full request body
has been received, and only keep the connection open on peer EOF when it
has. The body is only parsed as fast as the script consumes it, so a
request still in CLIENT_STATE_DATA may well have its complete body
buffered, e.g. while it is deferred waiting for a script slot:

- For a Content-Length body, the outstanding byte count
  (request.content_length) is compared against the bytes still buffered
  in the ustream (r.data_bytes).
- For a chunked body, the buffered bytes are walked from the current
  parser position without consuming them, and the body counts as complete
  once the terminal chunk and the end of the trailer section are buffered.

The read stream is a single compacted buffer and the peer's EOF is only
seen once everything it sent has been read into it, so the buffered data
covers all of the remaining body.

On EOF with the body still outstanding the connection now falls through to
client_close(), releasing both the connection and its script slot
immediately. A peer that sent the full body and then closed (the normal
half-close) is handled as before: the connection is kept and the pending
response is sent.

Fixes: https://github.com/openwrt/uhttpd/security/advisories/GHSA-p2hf-3hpj-hfpc
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>
<entry>
<title>ucode: add fallback to avoid compilation error</title>
<updated>2026-10-19T16:55:56Z</updated>
<author>
<name>Felix Fietkau</name>
</author>
<published>2026-10-19T16:54:31Z</published>
<link rel='alternate' type='text/html' href='https://git.openwrt.org/project/uhttpd/commit/?id=ca796d7dec5697a2d1a7a42be79677f239b64282'/>
<id>urn:sha1:ca796d7dec5697a2d1a7a42be79677f239b64282</id>
<content type='text'>
Do not fail on missing STATUS_BREAK check

Signed-off-by: Felix Fietkau &lt;nbd@nbd.name&gt;
</content>
</entry>
<entry>
<title>client: fix build without TLS support</title>
<updated>2026-10-09T22:14:41Z</updated>
<author>
<name>Jo-Philipp Wich</name>
</author>
<published>2026-10-09T22:09:28Z</published>
<link rel='alternate' type='text/html' href='https://git.openwrt.org/project/uhttpd/commit/?id=7a06a9d5623af115d21a415866d85e7e903dcba5'/>
<id>urn:sha1:7a06a9d5623af115d21a415866d85e7e903dcba5</id>
<content type='text'>
Building with -DTLS_SUPPORT=OFF fails:

  client.c:336:28: error: 'struct client' has no member named 'ssl'
  client.c:340:31: error: 'struct client' has no member named 'ssl'

The SNI redirect check dereferences struct client::ssl, which only
exists when TLS support is compiled in. Since an SNI server name can
only be observed on TLS connections, the check is meaningless in
non-TLS builds; replace it with a stub which always evaluates to
"no redirect", following the same pattern as the TLS helper stubs
in tls.h, leaving the call site untouched.

Fixes: 60f64bec40c8 ("tls: add SNI-based redirect support")
Signed-off-by: Jo-Philipp Wich &lt;jo@mein.io&gt;
</content>
</entry>
<entry>
<title>ubus: reject list requests with non-string params in uh_ubus_send_list()</title>
<updated>2026-10-09T22:14:41Z</updated>
<author>
<name>Jo-Philipp Wich</name>
</author>
<published>2026-10-09T22:07:36Z</published>
<link rel='alternate' type='text/html' href='https://git.openwrt.org/project/uhttpd/commit/?id=64dcf47aa7f84ae60db9a3950e5c4b8fbe5ed6f1'/>
<id>urn:sha1:64dcf47aa7f84ae60db9a3950e5c4b8fbe5ed6f1</id>
<content type='text'>
The "params" JSON-RPC policy is BLOBMSG_TYPE_UNSPEC, so any client may
send an array with numbers, booleans or nested objects as "list" params.
Each element was passed to ubus_lookup() as a NUL-terminated path string
via blobmsg_data(), but non-string blobmsg entries are not NUL terminated,
leading to an out-of-bounds read (and potentially a crash) on
attacker-controlled data, reachable without authentication.

Validate all array entries up front and answer with an "Invalid
parameters" error if any entry is not a properly terminated string,
matching how the "call" method rejects malformed params.

Fixes: 38aab5190777 ("ubus: implement list method to enumerate objects and signatures")
Reported-by: Mohammad Razavi &lt;razavi@dbug.om&gt;
Signed-off-by: Jo-Philipp Wich &lt;jo@mein.io&gt;
</content>
</entry>
<entry>
<title>Merge commit 'refs/pull/34/head' of https://github.com/openwrt/uhttpd</title>
<updated>2026-08-24T12:19:03Z</updated>
<author>
<name>Jo-Philipp Wich</name>
</author>
<published>2026-08-24T12:19:03Z</published>
<link rel='alternate' type='text/html' href='https://git.openwrt.org/project/uhttpd/commit/?id=373145f72c884c36a2b16f7f47e74ffae06bd754'/>
<id>urn:sha1:373145f72c884c36a2b16f7f47e74ffae06bd754</id>
<content type='text'>
</content>
</entry>
<entry>
<title>file: enforce Basic Auth realms for URL-prefix handlers</title>
<updated>2026-08-24T12:07:20Z</updated>
<author>
<name>Hauke Mehrtens</name>
</author>
<published>2026-08-24T12:03:22Z</published>
<link rel='alternate' type='text/html' href='https://git.openwrt.org/project/uhttpd/commit/?id=3aa7e1c7678281220470e24784bcdcfb71bda6cf'/>
<id>urn:sha1:3aa7e1c7678281220470e24784bcdcfb71bda6cf</id>
<content type='text'>
uh_handle_request() selects check_url dispatch handlers (lua, ucode and
ubus) via dispatch_find(url, NULL) and invokes them through
uh_invoke_handler() before reaching __handle_file_request(), which holds
the only uh_auth_check() call. As a result a configured Basic Auth realm
was never evaluated for those handlers: a request to a lua/ucode/ubus
prefix was served without authentication, and even invalid credentials
were ignored. CGI scripts are not affected because they register a
check_path handler and fall through to the authenticated file path, so
the same realm protected /cgi-bin/foo but silently not /api/foo.

Evaluate the matching realm against the request URL before invoking a
check_url handler. uh_auth_check() already emits the 401 challenge and
tears the request down on failure, so handlers stay consistent with the
static file and CGI paths. Deployments without a realm covering the
prefix are unaffected.

Link: https://github.com/openwrt/uhttpd/security/advisories/GHSA-5cgm-8h9x-v28c
Reported-by: @aramosf
Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Hauke Mehrtens &lt;hauke@hauke-m.de&gt;
Signed-off-by: Jo-Philipp Wich &lt;jo@mein.io&gt; [return style fix]
</content>
</entry>
<entry>
<title>client: set TCP_NODELAY on accepted sockets</title>
<updated>2026-08-05T15:30:11Z</updated>
<author>
<name>Ivan Kvashonkin</name>
</author>
<published>2026-07-26T14:00:18Z</published>
<link rel='alternate' type='text/html' href='https://git.openwrt.org/project/uhttpd/commit/?id=82b4c79ec266942251f95010eae700ddc33899ca'/>
<id>urn:sha1:82b4c79ec266942251f95010eae700ddc33899ca</id>
<content type='text'>
uh_accept_client() leaves the accepted socket with Nagle enabled, and a
response is emitted as one write() per header line plus one for the body.
The kernel therefore holds the small final segment: tcp_nagle_check()
allows a partial segment only if TCP_NODELAY is set, TCP_CORK is set, or
every small packet sent so far has been acknowledged (Minshall's
modification, tcp_minshall_check()). On a connection that has just
answered a request, the previous response is still unacknowledged, so the
next small body waits for the peer's delayed ACK. Every request after the
first on a keep-alive connection pays that.

Reproduced without LuCI, ubus, a session or a browser - a 5-byte static
file and four sequential requests on one connection, on 25.12 (x86_64) and
24.10.7 (uhttpd 2025.07.06~7e64e8ba-r4):

  req1 0.4 ms / 1.0 ms      (first on the connection)
  req2 41.5 ms / 42.2 ms
  req3 44.6 ms / 48.8 ms
  req4 44.6 ms / -
  fresh connection each time: 0.3-0.4 ms / 0.24-0.33 ms

strace shows uhttpd is not the slow part: it wrote response 2 within 0.4 ms
of response 1, while the client's next request only arrived 42.5 ms after
that write. The stall window is the peer's delayed ACK timer, so it depends
on the client; the figures above are Linux peers.

Both builds come from the 25.12.5 release SDK, and the unpatched build is
byte-identical to the published uhttpd-2026.06.16~7b1bec45-r1.apk, so the
before column is what is shipping. Same box, http_keepalive at its default:

  unpatched  0.35 / 42.9  / 44.8  / 44.8  ms
  patched    0.32 / 0.087 / 0.075 / 0.075 ms

LuCI is where this is visible, since its ubus JSON-RPC replies run a few
hundred bytes and a page issues several: warm in-place navigation over five
config pages went from 540 ms to 284 ms with keep-alive untouched.

The trade-off is packet count, and it is measured rather than assumed.
Disabling Nagle lets each of the eight writes leave as its own segment: the
four-request reproducer goes from 13 to 32 server-to-client segments, and a
full LuCI page load from 752 to 1211 packets for the same payload. For an
admin interface that is a few dozen extra packets per page view against
40 ms stalls, but the real answer is to coalesce the response into fewer
writes as well - nginx does both, enabling TCP_NODELAY when a connection
enters the keep-alive state. Worth noting for that: ustream already carries
a `more` flag from ustream_write() down to the backend, and ustream_fd_write()
ignores it, using write() rather than send() with MSG_MORE - while
ustream_vprintf() hardcodes more=false, so no printf-based header emission
can express it today.

Signed-off-by: Ivan Kvashonkin &lt;vizzlef@gmail.com&gt;
</content>
</entry>
</feed>
