#!/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")