<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-06-15T23:38:07Z</updated>
<entry>
<title>ubus: close connection on POST body parse error</title>
<updated>2026-06-15T23:38:07Z</updated>
<author>
<name>Hauke Mehrtens</name>
</author>
<published>2026-06-13T00:40:48Z</published>
<link rel='alternate' type='text/html' href='https://git.openwrt.org/project/uhttpd/commit/?id=7b1bec45826bd78c8afc993435bdc0f1df2fe399'/>
<id>urn:sha1:7b1bec45826bd78c8afc993435bdc0f1df2fe399</id>
<content type='text'>
uh_ubus_data_send() is only entered while there is still POST body data
to read. When the JSON body is delivered in several TCP segments and a
complete object has already been parsed (du-&gt;jsobj set), or the body
exceeds UH_UBUS_MAX_POST_SIZE, the error path emits a JSON-RPC parse
error and returns 0 without consuming the remaining declared
Content-Length bytes. The caller neither drained the rest of the body
nor closed the connection, so the unread body suffix was parsed as a
new HTTP request on the same keep-alive connection (request smuggling).

Set connection_close in the error path so the out-of-sync connection is
torn down instead of being reused.

Link: https://github.com/openwrt/uhttpd/security/advisories/GHSA-wgwp-64hh-f52p
Reported-by: @dyingc
Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Hauke Mehrtens &lt;hauke@hauke-m.de&gt;
</content>
</entry>
<entry>
<title>client: close connection on invalid chunk length</title>
<updated>2026-06-15T23:34:49Z</updated>
<author>
<name>Hauke Mehrtens</name>
</author>
<published>2026-06-13T00:40:40Z</published>
<link rel='alternate' type='text/html' href='https://git.openwrt.org/project/uhttpd/commit/?id=b78f518478794e16ba3568fc1258e40bf3e8eb3b'/>
<id>urn:sha1:b78f518478794e16ba3568fc1258e40bf3e8eb3b</id>
<content type='text'>
When client_poll_post_data() encounters an invalid chunk-length line it
consumes the chunk-size line and clears content_length and
transfer_chunked, which makes the request look complete. The remaining
buffered body was neither drained nor was connection_close set, so the
keep-alive parser re-entered CLIENT_STATE_INIT and parsed the leftover
body bytes as a new HTTP request (request smuggling). An oversized
chunk length (e.g. 80000000) can be used to align an embedded request
on the chunk boundary.

Force connection_close in the invalid chunk-length branch so the
out-of-sync connection is torn down instead of being reused.

Link: https://github.com/openwrt/uhttpd/security/advisories/GHSA-p55c-rmhc-qfm5
Reported-by: @dyingc
Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Hauke Mehrtens &lt;hauke@hauke-m.de&gt;
</content>
</entry>
<entry>
<title>client: reject unhandled Transfer-Encoding values</title>
<updated>2026-06-15T23:30:15Z</updated>
<author>
<name>Hauke Mehrtens</name>
</author>
<published>2026-06-13T00:40:15Z</published>
<link rel='alternate' type='text/html' href='https://git.openwrt.org/project/uhttpd/commit/?id=ae015e099986ceace44975cb69629841a2d58d37'/>
<id>urn:sha1:ae015e099986ceace44975cb69629841a2d58d37</id>
<content type='text'>
client_parse_header() lowercases header names but compared the
Transfer-Encoding value with a case-sensitive strcmp(val, "chunked").
RFC 9112 6.1 requires transfer-coding tokens to be matched
case-insensitively, so a request with "Transfer-Encoding: Chunked" was
accepted but chunked framing was never enabled. The declared body was
then left unread and, on a keep-alive connection, parsed as the next
HTTP request (request smuggling).

Match "chunked" with strcasecmp and reject any other transfer-coding we
do not implement with 501, instead of silently ignoring it and losing
track of the message framing.

Link: https://github.com/openwrt/uhttpd/security/advisories/GHSA-mcfg-c4r7-pjpf
Reported-by: @dyingc
Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Hauke Mehrtens &lt;hauke@hauke-m.de&gt;
</content>
</entry>
<entry>
<title>auth: classify $p$ lookups by account state</title>
<updated>2026-05-20T00:55:10Z</updated>
<author>
<name>Hauke Mehrtens</name>
</author>
<published>2026-05-17T22:01:16Z</published>
<link rel='alternate' type='text/html' href='https://git.openwrt.org/project/uhttpd/commit/?id=1b624f8f814ed568608d756512892416e0431d77'/>
<id>urn:sha1:1b624f8f814ed568608d756512892416e0431d77</id>
<content type='text'>
uh_auth_add resolves '$p$&lt;user&gt;' password lines by reading the named
account's stored credential from /etc/shadow (or /etc/passwd as a
fallback). The previous code copied that string verbatim into
realm-&gt;pass without checking whether it was a usable crypt(3) hash.
This broke for two distinct shadow states:

 1. Locked or placeholder credentials ("*", "x", "!", "!!", "*LK*",
    "*NP*", "!hash" lock prefix). In uh_auth_check the plaintext
    compare branch fires whenever realm-&gt;pass does not start with '$',
    so any client sending the placeholder verbatim as the password
    matched and authenticated. On OpenWrt every system account except
    root has '*' or 'x' in /etc/shadow by default, so a realm configured as
    '/cgi-bin/admin:adminuser:$p$daemon' (the pattern the example UCI
    in package/network/services/uhttpd/files/uhttpd.config documents)
    authenticated any request with password '*' and ran the CGI as
    root.

 2. Empty password (the default OpenWrt root entry, "root:::..."):
    handled correctly by the existing empty-pass drop but silently,
    leaving admins unaware that '$p$root' on a fresh image produces a
    public URL.

Match login(1) semantics: locked or placeholder accounts deny all
access, accounts with no password set permit access without
authentication, accounts with a real hash require it. For each
non-hash case log a distinct warning so admins notice the silent
state. A '$p$' reference to a non-existent account is treated as a
config typo: deny all access (consistent with the admin's stated
intent of requiring auth) and log a warning.

For the locked case, bind the realm to a sentinel that starts with
'$' (skipping the plaintext compare branch) and cannot be produced by
crypt(3) (failing the hash branch), so every request returns 401
instead of falling through to a missing-realm public response.

Reported-by: Amit Pinchasi &lt;amitpinchasi123@gmail.com&gt;
Co-Authored-By: Claude Opus 4.7 (1M context) &lt;noreply@anthropic.com&gt;
Link: https://github.com/openwrt/uhttpd/pull/25
Signed-off-by: Hauke Mehrtens &lt;hauke@hauke-m.de&gt;
</content>
</entry>
<entry>
<title>cgi, file: fix crash due to field_len type mismatch with libubox</title>
<updated>2026-05-20T00:44:00Z</updated>
<author>
<name>Andy Chiang</name>
</author>
<published>2026-05-18T09:40:28Z</published>
<link rel='alternate' type='text/html' href='https://git.openwrt.org/project/uhttpd/commit/?id=6ab9abb56bcb28684f382f7a94c170fad02348ea'/>
<id>urn:sha1:6ab9abb56bcb28684f382f7a94c170fad02348ea</id>
<content type='text'>
In libubox commit https://github.com/openwrt/libubox/commit/9b488010c4a74202a85ed24e01108fe069bd42e4,
the type of `alloc_len` in `calloc_a` was changed to `size_t`.

Since uhttpd still defined `field_len` as returning `int`, this type mismatch caused uhttpd to crash.
So change `field_len` type to `size_t` and add NULL check.

Fixes: https://github.com/openwrt/luci/issues/8629
Fixes: https://github.com/openwrt/libubox/pull/45

Signed-off-by: Andy Chiang &lt;AndyChiang_git@outlook.com&gt;
Link: https://github.com/openwrt/uhttpd/pull/24
Signed-off-by: Hauke Mehrtens &lt;hauke@hauke-m.de&gt;
</content>
</entry>
<entry>
<title>auth: replace strcmp with constant-time password comparison</title>
<updated>2026-05-15T00:54:17Z</updated>
<author>
<name>Hauke Mehrtens</name>
</author>
<published>2026-03-31T20:58:00Z</published>
<link rel='alternate' type='text/html' href='https://git.openwrt.org/project/uhttpd/commit/?id=6fadf0da50509a510ac4f85d2adb70a83de2e1fc'/>
<id>urn:sha1:6fadf0da50509a510ac4f85d2adb70a83de2e1fc</id>
<content type='text'>
strcmp short-circuits on the first differing byte, leaking password
match progress via measurable response time differences. Add
uh_pass_compare() which XORs all bytes unconditionally and only
returns true when both length and content match, preventing a
timing-based password oracle attack.

Co-Authored-By: Claude Sonnet 4.6 &lt;noreply@anthropic.com&gt;
Signed-off-by: Hauke Mehrtens &lt;hauke@hauke-m.de&gt;
</content>
</entry>
<entry>
<title>auth: do not accept stored crypt hash as plaintext password</title>
<updated>2026-05-15T00:29:10Z</updated>
<author>
<name>Hauke Mehrtens</name>
</author>
<published>2026-05-03T20:54:03Z</published>
<link rel='alternate' type='text/html' href='https://git.openwrt.org/project/uhttpd/commit/?id=b33ca5d377189f0fa7f6ee1b325ec08078a64c91'/>
<id>urn:sha1:b33ca5d377189f0fa7f6ee1b325ec08078a64c91</id>
<content type='text'>
uh_auth_check first compared the supplied password to the stored
credential with strcmp() and only fell through to crypt() on
mismatch. The strcmp branch is meant to support plaintext
credentials in /etc/httpd.conf, but it also matched when the stored
credential is a crypt hash and the request supplies that exact hash
as the password.

When the password line uses the '$p$&lt;user&gt;' indirection, realm-&gt;pass
is populated from /etc/shadow or /etc/passwd. Anyone who manages to
read the hash (a backup, an unrelated disclosure bug, an offline
copy) can therefore authenticate by sending the hash itself,
bypassing the need to recover the underlying password.

Skip the plaintext compare when realm-&gt;pass starts with '$', which
is the marker for every modular crypt(3) format (md5crypt, sha256,
sha512, bcrypt, yescrypt, ...). Plaintext credentials configured
via httpd.conf still work; stored hashes are only honored through
crypt().

Co-Authored-By: Claude Opus 4.7 (1M context) &lt;noreply@anthropic.com&gt;
Signed-off-by: Hauke Mehrtens &lt;hauke@hauke-m.de&gt;
</content>
</entry>
<entry>
<title>client: parse chunked transfer chunk size safely</title>
<updated>2026-05-15T00:29:10Z</updated>
<author>
<name>Hauke Mehrtens</name>
</author>
<published>2026-03-31T20:56:47Z</published>
<link rel='alternate' type='text/html' href='https://git.openwrt.org/project/uhttpd/commit/?id=9404e6c62bb7dc452a294a62c33eb18beb9ef433'/>
<id>urn:sha1:9404e6c62bb7dc452a294a62c33eb18beb9ef433</id>
<content type='text'>
The chunk-size hex field is parsed into content_length (int). The
previous strtoul could wrap negative-looking inputs into huge
positive values; switching to strtol exposes negatives but leaves
the 64-bit INT_MAX overflow window open, where a long value above
INT_MAX truncates implementation-defined when narrowed.

Parse into a long, check errno for strtol overflow, and reject any
chunk size above INT_MAX. On malformed or out-of-range input the
chunked transfer state is torn down as before.

Co-Authored-By: Claude Sonnet 4.6 &lt;noreply@anthropic.com&gt;
Signed-off-by: Hauke Mehrtens &lt;hauke@hauke-m.de&gt;
</content>
</entry>
<entry>
<title>client: parse Content-Length safely</title>
<updated>2026-05-15T00:29:10Z</updated>
<author>
<name>Hauke Mehrtens</name>
</author>
<published>2026-03-31T20:56:15Z</published>
<link rel='alternate' type='text/html' href='https://git.openwrt.org/project/uhttpd/commit/?id=2c869c094c25e1b6b7c1ba786f22280a6c153447'/>
<id>urn:sha1:2c869c094c25e1b6b7c1ba786f22280a6c153447</id>
<content type='text'>
content_length is declared int but the previous strtoul cast silently
wrapped negative inputs into huge positive values, making the &lt; 0
guard unreachable. Switching to strtol exposes negatives, but on
64-bit platforms a value above INT_MAX still passes strtol's own
range check and truncates implementation-defined when narrowed to int.

Parse into a long, check errno for strtol overflow, and reject any
value above INT_MAX or below zero before assigning. The error path
returns 400 Bad Request as before.

Pull in errno.h and limits.h for the new checks.

Co-Authored-By: Claude Sonnet 4.6 &lt;noreply@anthropic.com&gt;
Signed-off-by: Hauke Mehrtens &lt;hauke@hauke-m.de&gt;
</content>
</entry>
<entry>
<title>main: fix daemonization stdio redirection and fd leak</title>
<updated>2026-05-15T00:29:10Z</updated>
<author>
<name>Hauke Mehrtens</name>
</author>
<published>2026-04-13T08:31:21Z</published>
<link rel='alternate' type='text/html' href='https://git.openwrt.org/project/uhttpd/commit/?id=778ccbbf5f8aa90e368100897f59322314287ab4'/>
<id>urn:sha1:778ccbbf5f8aa90e368100897f59322314287ab4</id>
<content type='text'>
Two bugs in the daemon child setup:

1. The condition `cur_fd &gt; 0` fails to redirect stdin/stdout/stderr
   when open("/dev/null") returns fd 0 (possible if stdin was already
   closed before daemonising).  Change to `cur_fd &gt;= 0` to handle
   that case correctly.

2. After the three dup2() calls the original file descriptor
   `cur_fd` is never closed.  If open() returned a descriptor number
   greater than 2 (the common case), that descriptor is leaked into
   the server process for its entire lifetime.  Add `close(cur_fd)`
   when `cur_fd &gt; 2` to release it.

3. Change the open() flags from O_WRONLY to O_RDWR so that fd 0
   (stdin) is also opened in a readable state, matching normal daemon
   practice.

Co-Authored-By: Claude Sonnet 4.6 &lt;noreply@anthropic.com&gt;
Signed-off-by: Hauke Mehrtens &lt;hauke@hauke-m.de&gt;
</content>
</entry>
</feed>
