
A behavior-preserving fix for CVE-2025-60876, an HTTP header-injection vulnerability in BusyBox wget, plus the proof-of-concept that demonstrates it and the artifacts for submitting the fix upstream to BusyBox.
BusyBox wget copies the URL path and query into the HTTP request line verbatim. A URL that carries a raw CR (0x0D), LF (0x0A), or other control byte can split the request line and inject attacker-controlled headers. A space (0x20) has the same effect: it breaks the METHOD SP request-target SP HTTP/1.1 framing.
Affected: BusyBox through 1.37.0 (the pkgver shipped on every in-support Alpine branch). Reported on the BusyBox mailing list in August 2025; CVE assigned as CVE-2025-60876 (NVD CVSS 6.5, Medium).
Two patches were already proposed upstream (Takeuchi Yuma, 2025-08; Radoslav Kolev, 2025-11). Both reject an offending URL and call bb_error_msg_and_die. That stops the injection, but it also rejects a plain space, so a URL like http://example.org/foo bar that used to work now errors. That regression is why the fix has not been picked up downstream. Alpine is holding its backport on it (work item 17872).
This fix handles the path and the host differently, matching how GNU wget and curl behave.
Path: percent_encode_target() in networking/wget.c percent-encodes control bytes (0x00 through 0x1f), space (0x20), and DEL (0x7f) in the request-target before the request line is built:
http://example.org/foo bar is sent as /foo%20bar, matching GNU wget and curl. No regression.% is left unchanged, so an already-encoded path is not double-encoded.Host: the same bytes are rejected in the URL host. 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 (GET http://host/path) 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 too.
The change is confined to networking/wget.c and adds one helper.
Out of scope: the FTP control channel (ftpcmd sends target->path and the user/password raw, an upstream-acknowledged TODO) is a separate injection class, not part of CVE-2025-60876, and is not addressed here. It should be tracked separately.
Built against BusyBox 1.37.0 with the same config Alpine uses (CONFIG_WERROR and CONFIG_TC off). A local listener captures the exact bytes wget puts on the wire.
Full capture: test/poc-output.txt. The four BusyBox testsuite/wget tests all pass against the patched build (test/testsuite-output.txt); the other full-suite failures noted there are unrelated (mount/taskset need root, and a few known restricted-container quirks). Size impact on aarch64 defconfig is +185 bytes (wget_main +142, .rodata +43, no symbols added or removed), measured with scripts/bloat-o-meter and recorded in cover-letter.txt.
test/finalize.sh runs inside an alpine:edge container with Docker and reproduces everything (PoC, size, testsuite, patch):
docker run --rm -v "$PWD:/work" alpine:edge sh /work/test/finalize.sh
It downloads BusyBox 1.37.0, builds it vanilla (showing the injection directly and via a proxy), applies the fix, rebuilds, replays the request cases above, runs bloat-o-meter and the wget testsuite, and writes the patch. test/driver.sh is a lighter PoC-only variant.
BusyBox is a mailing-list project. The fix goes to [email protected] via git send-email, as a [PATCH v3] reply on Radoslav Kolev's November 2025 thread, crediting the prior reporters and reviewers. See cover-letter.txt. The fix is GPLv2, the same license as BusyBox.
| Input URL | Vanilla 1.37.0 | Patched |
|---|
/x + CRLF + Evil: injected | Evil: injected arrives as a real header (vulnerable) | GET /x%0D%0AEvil:%20injected HTTP/1.1, no injected header |
/foo bar | sends a literal space | GET /foo%20bar HTTP/1.1 (no regression) |
/foo%20bar (already encoded) | unchanged | GET /foo%20bar HTTP/1.1 (not %2520) |
/normal | unchanged | GET /normal HTTP/1.1 (unchanged) |
proxy, host = h + CRLF + PInjected: 1 | PInjected: 1 arrives as a real header (vulnerable) | dies with bad character in URL host, nothing sent |
proxy, host = example.test | normal | GET http://example.test/p HTTP/1.1 (unchanged) |
| File | What it is |
|---|
wget-cve-2025-60876.patch | The fix as a git format-patch against BusyBox networking/wget.c, ready to send |
commit-message.txt | The commit message (also the patch header) |
cover-letter.txt | The [PATCH v3] mailing-list cover letter that revives the existing thread |
apply_fix.py | Generator that applies the change to a networking/wget.c (used to produce the patch) |
test/serve_once.py | Single-connection listener that echoes the raw HTTP request |
test/driver.sh | Builds vanilla and patched BusyBox and runs the direct + proxy before/after PoC |
test/finalize.sh | Full run: before/after PoC, bloat-o-meter, testsuite/wget, and regenerates the patch |
test/poc-output.txt | Captured before/after request bytes (direct and proxy) |
test/testsuite-output.txt | BusyBox testsuite/wget results against the patched build |