[PATCH v3] wget: percent-encode control characters and space in the request URL
Hayden Barnes via busybox <[email protected]> Fri, 12 Jun 2026 19:24:57 -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. 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. Signed-off-by: Hayden Barnes <[email protected]> --- 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 (the host goes verbatim into the absolute-form request-target and the Host: header and is not resolved locally). v1/v2 rejected the whole URL. Size, aarch64 defconfig (scripts/bloat-o-meter): add/remove: 0/0 grow/shrink: 2/0 up/down: 185/0 Total: 185 bytes Tested with a local listener, direct and via http_proxy: vanilla injects a header through a CRLF path and through a CRLF proxy host; the patched build encodes the path (GET /x%0D%0AEvil:%20injected HTTP/1.1, no injected header) and dies on the bad host (bad character in URL host); /foo bar -> /foo%20bar; an already-encoded path is unchanged; the testsuite/wget tests pass. 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..8e59b42 100644 --- a/networking/wget.c +++ b/networking/wget.c @@ -622,6 +622,33 @@ 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 = "0123456789ABCDEF"; + 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]; + *d++ = hex[c & 0xf]; + } else { + *d++ = c; + } + } + *d = '\0'; + return buf; +} + static char *get_sanitized_hdr(FILE *fp) { char *s, *hdrval; @@ -1220,15 +1247,31 @@ 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); + } + free(req_target); } if (!USR_HEADER_HOST) SENDFMT(sfp, "Host: %s\r\n", target.host); -- 2.54.0