README.md
Rendering markdown...
From 71e752906d8de12432d1ad8f839a7ad10917f3ae Mon Sep 17 00:00:00 2001
From: Hayden Barnes <[email protected]>
Date: Fri, 12 Jun 2026 23:03:46 +0000
Subject: [PATCH v3] wget: percent-encode control characters and space in the
request URL
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]>
---
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