curl · Report #3751715
Heap-OOB read in urlapi `redirect_url()` via `CURLU_GUESS_SCHEME` + `CURLU_NO_GUESS_SCHEME` flow
How it was found
Hi all,
We've found an issue in lib/urlapi.c where redirect_url() reads past the end of a heap buffer when the source URL it operates on lacks a "scheme://" prefix. This is reachable through documented public APIs (curl_url_set) when the caller mixes CURLU_GUESS_SCHEME with a subsequent CURLU_NO_GUESS_SCHEME set on the same handle.
redirect_url() at lib/urlapi.c:1241 computes:
const char *protsep = base + strlen(scheme) + 3;
assuming base begins with "<scheme>://". But urlget_url() at lib/urlapi.c:1527-1530 deliberately omits the "scheme://" prefix when CURLU_NO_GUESS_SCHEME is set and u->guessed_scheme is true:
if(!(flags & CURLU_NO_GUESS_SCHEME) || !u->guessed_scheme)
curl_msnprintf(schemebuf, sizeof(schemebuf), "%s://", scheme);
else
schemebuf[0] = 0;
set_url() forwards the caller's flags to urlget_url and then to redirect_url. With a short host like "a.b", base is the 4-byte string "a.b/", so protsep ends up 7 bytes past the start of a 4-byte buffer, i.e. 2-3 bytes past the NUL terminator. The subsequent strchr(protsep, '/') / '?' / '#' calls walk into adjacent heap memory.
The bug requires two commits to coexist. 655d44d139 (2024-06-01, "urlapi: add CURLU_NO_GUESS_SCHEME") first taught urlget_url to emit URLs without the "scheme://" prefix when NO_GUESS_SCHEME is set on a previously-guessed scheme. Then bc24c60512 (2025-02-26, "urlapi: fix redirect from file:// with query, and simplify") added the unconditional protsep = base + strlen(scheme) + 3 pointer arithmetic in redirect_url that depends on a "scheme://" prefix being present. (The recent commit f9b9d3b141 addressed a sibling NULL-scheme variant by introducing a DEFAULT_SCHEME fallback, but the missing-prefix-due-to-guessed-scheme variant remains unfixed.)
Reproducer
Save as poc.c:
#include <stdio.h>
#include <curl/curl.h>
int main(void) {
CURLU *u = curl_url();
curl_url_set(u, CURLUPART_URL, "a.b", CURLU_GUESS_SCHEME);
curl_url_set(u, CURLUPART_URL, "/x", CURLU_NO_GUESS_SCHEME);
curl_url_cleanup(u);
return 0;
}
Build curl with ASan:
autoreconf -fi
mkdir build && cd build
CFLAGS='-O1 -g -fsanitize=address' LDFLAGS='-fsanitize=address' CC=clang \
../configure --with-openssl --enable-debug --enable-maintainer-mode --disable-shared
make -j
clang -fsanitize=address -g -I../include poc.c \
./lib/.libs/libcurl.a -lssl -lcrypto -lz -lnghttp2 -lidn2 -lzstd \
-lbrotlidec -lldap -llber -o poc
./poc
Observed output
=================================================================
==658666==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x70ef1b7e00c0 at pc 0x571b3b990b40 bp 0x7ffea09786b0 sp 0x7ffea0977e78
READ of size 34 at 0x70ef1b7e00c0 thread T0
[..]
(p.s. the above snippet is limited because for whatever reason, the poc simply hangs when running it after that ASAN message; absolutely no clue, and my local LLM couldn't tell me why either).
Suggested fix
Strip CURLU_NO_GUESS_SCHEME from the flags set_url passes through to its internal curl_url_get, so the rendered base always carries the "scheme://" prefix redirect_url assumes:
--- a/lib/urlapi.c
+++ b/lib/urlapi.c
@@ -1732,7 +1732,8 @@ static CURLUcode set_url(CURLU *u, CURLUPart part, size_t nalloc,
/* if the old URL is incomplete (we cannot get an absolute URL in
'oldurl'), replace the existing with the new */
- uc = curl_url_get(u, CURLUPART_URL, &oldurl, flags);
+ /* always include "scheme://" so redirect_url's pointer arithmetic is valid */
+ uc = curl_url_get(u, CURLUPART_URL, &oldurl, flags & ~CURLU_NO_GUESS_SCHEME);
if(uc == CURLUE_OUT_OF_MEMORY)
return uc;
else if(uc)
Cheers, AISLE Research Team
Impact
buffer overread.