[PATCH v4] wget: percent-encode control characters and space in the request URL
Hayden Barnes via busybox <[email protected]> Mon, 15 Jun 2026 21:15:17 -0400
| Newsgroups | gmane.linux.busybox |
|---|---|
| Message-ID | <[email protected]> |
wget copies the URL path and query into the HTTP request line verbatim, so a URL that contains a raw CR, LF, or other control byte can split the request line and inject headers (CVE-2025-60876). A space has the same effect: it breaks the "METHOD SP request-target SP HTTP/1.1" framing. The patches proposed earlier on the list reject the whole URL and call bb_error_msg_and_die. That stops the injection, but it changes behavior. A URL like http://example.org/foo bar used to be sent (the server gets the literal space) and now fails outright, because the space is rejected along with the control characters. Handle the path and the host differently, matching how GNU wget and curl behave: - Path: percent-encode the offending octets in the request-target. Control bytes (0x00 through 0x1f), space (0x20), and DEL (0x7f) become %XX. Every other byte, including an existing '%', passes through unchanged, so an already-encoded path is not double-encoded and http://example.org/foo bar is sent as /foo%20bar. CR and LF can no longer reach the request line. - Host: reject those same bytes. A hostname cannot legitimately contain control characters or a space, and percent-encoding is not defined for the authority component. This matters in proxy mode: the host is placed in the absolute-form request-target and the Host: header but is not resolved locally, so a raw CR or LF in the host would otherwise inject. GNU wget (since CVE-2017-6508) and curl reject control characters in the host as well. The change is confined to networking/wget.c and adds one small helper. The encoder reuses bb_hexdigits_upcase[] for the hex table and frees the request target only under FEATURE_CLEAN_UP; these size refinements were proposed by Roberto A. Foglietta. Based on the analysis and patches from Takeuchi Yuma (2025-08) and Radoslav Kolev (2025-11), changed from rejecting the whole URL to encoding the path and rejecting only the host, to avoid the behavior regression raised in review. Fixes CVE-2025-60876. Suggested-by: Roberto A. Foglietta <[email protected]> Signed-off-by: Hayden Barnes <[email protected]> --- v4: fold in Roberto Foglietta's p2v2 size refinements - bb_hexdigits_upcase[] for the hex table, the encode loop collapsed to a single store, and free the request target only under FEATURE_CLEAN_UP. Kept the (const unsigned char *) cast on the path pointer; dropping it (as p2v2 did) makes networking/wget.o fail -Werror=pointer-sign. Verified clean: wget.o builds under CONFIG_WERROR=y and testsuite/wget passes on current git master. v3: encode the request-target path instead of rejecting it (so http://host/foo bar still works, sent as /foo%20bar), which removes the behavior regression that held the v1/v2 patches downstream. Reject control/space/DEL only in the host, which a path-only encode would miss in proxy mode. v1/v2 rejected the whole URL. Prior submissions: Kolev v2 [1], Takeuchi [2]; Alpine tracking [3]. [1] https://lists.busybox.net/pipermail/busybox/2025-November/091840.html [2] https://lists.busybox.net/pipermail/busybox/2025-August/091710.html [3] https://gitlab.alpinelinux.org/alpine/aports/-/work_items/17872 networking/wget.c | 61 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 52 insertions(+), 9 deletions(-) diff --git a/networking/wget.c b/networking/wget.c index ec37677..4cc6212 100644 --- a/networking/wget.c +++ b/networking/wget.c @@ -622,6 +622,32 @@ static void parse_url(const char *src_url, struct host_info *h) */ } +/* RFC 3986: the request-target on the HTTP request line must not carry raw + * control characters or spaces - a crafted URL could otherwise split the + * request line and inject headers (CVE-2025-60876). Percent-encode such octets + * (controls, space, DEL) instead of sending them verbatim. '%' and other + * printable bytes pass through unchanged, so already-encoded sequences are not + * double-encoded and "/foo bar" is sent as "/foo%20bar", matching wget/curl. */ +static char *percent_encode_target(const char *path) +{ + const char *hex = bb_hexdigits_upcase; + const unsigned char *s = (const unsigned char *)path; + char *buf, *d; + + d = buf = xmalloc(strlen(path) * 3 + 1); + while (*s) { + unsigned char c = *s++; + if (c <= ' ' || c == 0x7f) { + *d++ = '%'; + *d++ = hex[c >> 4]; + c = hex[c & 0xf]; + } + *d++ = c; + } + *d = '\0'; + return buf; +} + static char *get_sanitized_hdr(FILE *fp) { char *s, *hdrval; @@ -1220,15 +1246,32 @@ static void download_one_url(const char *url) /* ssl (https) support is not configured */ sfp = open_socket(lsa); #endif - /* Send HTTP request */ - if (use_proxy) { - SENDFMT(sfp, "GET %s://%s/%s HTTP/1.1\r\n", - target.protocol, target.host, - target.path); - } else { - SENDFMT(sfp, "%s /%s HTTP/1.1\r\n", - (option_mask32 & WGET_OPT_POST) ? "POST" : "GET", - target.path); + /* Send HTTP request. The request-target path is percent-encoded so a + * crafted URL cannot split the request line or inject headers + * (CVE-2025-60876): "/foo bar" is sent as "/foo%20bar". The host is sent + * verbatim in the proxy request-target and the Host: header, and in proxy + * mode is not resolved locally, so reject control chars and space there + * (a hostname can never legitimately contain them). */ + { + const unsigned char *hp = (const unsigned char *)target.host; + char *req_target; + while (*hp) { + if (*hp <= ' ' || *hp == 0x7f) + bb_simple_error_msg_and_die("bad character in URL host"); + hp++; + } + req_target = percent_encode_target(target.path); + if (use_proxy) { + SENDFMT(sfp, "GET %s://%s/%s HTTP/1.1\r\n", + target.protocol, target.host, + req_target); + } else { + SENDFMT(sfp, "%s /%s HTTP/1.1\r\n", + (option_mask32 & WGET_OPT_POST) ? "POST" : "GET", + req_target); + } + if (ENABLE_FEATURE_CLEAN_UP) + free(req_target); } if (!USR_HEADER_HOST) SENDFMT(sfp, "Host: %s\r\n", target.host); -- 2.54.0