README.md
Rendering markdown...
#!/usr/bin/env python3
"""Apply the RFC-correct fix for CVE-2025-60876 to busybox networking/wget.c.
Two parts:
- percent-encode control chars/space/DEL in the request-target PATH at send
time (preserves "/foo bar" -> "/foo%20bar", no double-encoding of '%'),
- reject control chars/space in the URL HOST, which is sent verbatim in the
proxy request-target and the Host: header and is not resolved locally under
a proxy (a hostname can never legitimately contain those bytes).
"""
import sys
path = sys.argv[1]
s = open(path, encoding='utf-8').read()
HELPER = '''/* 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)
{
\tconst char *hex = "0123456789ABCDEF";
\tconst unsigned char *s = (const unsigned char *)path;
\tchar *buf, *d;
\td = buf = xmalloc(strlen(path) * 3 + 1);
\twhile (*s) {
\t\tunsigned char c = *s++;
\t\tif (c <= ' ' || c == 0x7f) {
\t\t\t*d++ = '%';
\t\t\t*d++ = hex[c >> 4];
\t\t\t*d++ = hex[c & 0xf];
\t\t} else {
\t\t\t*d++ = c;
\t\t}
\t}
\t*d = '\\0';
\treturn buf;
}
'''
anchor = 'static char *get_sanitized_hdr(FILE *fp)\n'
assert s.count(anchor) == 1, "helper anchor not found uniquely"
s = s.replace(anchor, HELPER + anchor, 1)
OLD = '''\t\t/* Send HTTP request */
\t\tif (use_proxy) {
\t\t\tSENDFMT(sfp, "GET %s://%s/%s HTTP/1.1\\r\\n",
\t\t\t\ttarget.protocol, target.host,
\t\t\t\ttarget.path);
\t\t} else {
\t\t\tSENDFMT(sfp, "%s /%s HTTP/1.1\\r\\n",
\t\t\t\t(option_mask32 & WGET_OPT_POST) ? "POST" : "GET",
\t\t\t\ttarget.path);
\t\t}
'''
NEW = '''\t\t/* Send HTTP request. The request-target path is percent-encoded so a
\t\t * crafted URL cannot split the request line or inject headers
\t\t * (CVE-2025-60876): "/foo bar" is sent as "/foo%20bar". The host is sent
\t\t * verbatim in the proxy request-target and the Host: header, and in proxy
\t\t * mode is not resolved locally, so reject control chars and space there
\t\t * (a hostname can never legitimately contain them). */
\t\t{
\t\t\tconst unsigned char *hp = (const unsigned char *)target.host;
\t\t\tchar *req_target;
\t\t\twhile (*hp) {
\t\t\t\tif (*hp <= ' ' || *hp == 0x7f)
\t\t\t\t\tbb_simple_error_msg_and_die("bad character in URL host");
\t\t\t\thp++;
\t\t\t}
\t\t\treq_target = percent_encode_target(target.path);
\t\t\tif (use_proxy) {
\t\t\t\tSENDFMT(sfp, "GET %s://%s/%s HTTP/1.1\\r\\n",
\t\t\t\t\ttarget.protocol, target.host,
\t\t\t\t\treq_target);
\t\t\t} else {
\t\t\t\tSENDFMT(sfp, "%s /%s HTTP/1.1\\r\\n",
\t\t\t\t\t(option_mask32 & WGET_OPT_POST) ? "POST" : "GET",
\t\t\t\t\treq_target);
\t\t\t}
\t\t\tfree(req_target);
\t\t}
'''
assert s.count(OLD) == 1, "send block not found uniquely"
s = s.replace(OLD, NEW, 1)
open(path, 'w', encoding='utf-8', newline='\n').write(s)
print("apply_fix: helper + host-reject + path-encode applied")