8000 wget: backport CVE-2026-15146 fix (FTP PASV address SSRF) by bryan-minimal · Pull Request #916 · gominimal/pkgs · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions packages/wget/CVE-2026-15146.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
From 4f85853f641863d5915786a8413e1a213726a62b Mon Sep 17 00:00:00 2001
From: Acts1631 <acts1631kjv@proton.me>
Date: Sun, 5 Jul 2026 17:22:55 -0400
Subject: [PATCH] ftp: validate PASV/LPSV response address against control
connection peer

* src/ftp-basic.c (ftp_pasv): Reject if peer address doesn't match advertised
address,
(ftp_lpsv): Likewise.

ftp_pasv() and ftp_lpsv() copied the IP address and port advertised in
the server's 227 response without checking that it matched the peer
of the control connection. A malicious or compromised FTP server
could therefore direct wget's data connection to an arbitrary host and
port of its choosing (e.g. an internal service unreachable from the
attacker directly), which is a server-side request forgery.

ftp_epsv() was already safe since it only extracts a port and reuses
the pre-filled control-connection address.

Fix ftp_pasv() and ftp_lpsv() the same way: capture the control
connection's peer address via socket_ip_address() before parsing the
response, and reject the response (FTPINVPASV) if the parsed address
does not match.

Verified with a fake FTP server that returns a PASV response pointing
at a different loopback address (127.0.0.2 instead of the real peer
127.0.0.1): before the fix wget connects to the spoofed address, after
the fix it rejects the response with "Cannot parse PASV response."
Legitimate transfers using a correctly-addressed PASV response
continue to work.

Copyright-paperwork-exempt: Yes
---
src/ftp-basic.c | 40 ++++++++++++++++++++++++++++++++++++++++
1 file changed, 40 insertions(+)

diff --git a/src/ftp-basic.c b/src/ftp-basic.c
index 4870256a..0f4bb821 100644
--- a/src/ftp-basic.c
+++ b/src/ftp-basic.c
@@ -623,10 +623,19 @@ ftp_pasv (int csock, ip_address *addr, int *port)
int nwritten, i;
uerr_t err;
unsigned char tmp[6];
+ ip_address peer_addr;

assert (addr != NULL);
assert (port != NULL);

+ /* Remember who we are talking to on the control connection, so that
+ the address returned in the PASV response can be checked below.
+ Accepting an arbitrary server-supplied address would let a
+ malicious FTP server redirect our data connection to any host of
+ its choosing (SSRF). */
+ if (!socket_ip_address (csock, &peer_addr, ENDPOINT_PEER))
+ return FTPINVPASV;
+
xzero (*addr);

/* Form the request. */
@@ -677,6 +686,16 @@ ftp_pasv (int csock, ip_address *addr, int *port)
memcpy (IP_INADDR_DATA (addr), tmp, 4);
*port = ((tmp[4] << 8) & 0xff00) + tmp[5];

+ /* Reject the response if the advertised address does not match the
+ control connection's peer. */
+ if (peer_addr.family != AF_INET
+ || memcmp (IP_INADDR_DATA (addr), IP_INADDR_DATA (&peer_addr), 4) != 0)
+ {
+ xzero (*addr);
+ *port = 0;
+ return FTPINVPASV;
+ }
+
return FTPOK;
}

@@ -692,10 +711,19 @@ ftp_lpsv (int csock, ip_address *addr, int *port)
uerr_t err;
unsigned char tmp[16];
unsigned char tmpprt[2];
+ ip_address peer_addr;

assert (addr != NULL);
assert (port != NULL);

+ /* Remember who we are talking to on the control connection, so that
+ the address returned in the LPSV response can be checked below.
+ Accepting an arbitrary server-supplied address would let a
+ malicious FTP server redirect our data connection to any host of
+ its choosing (SSRF). */
+ if (!socket_ip_address (csock, &peer_addr, ENDPOINT_PEER))
+ return FTPINVPASV;
+
xzero (*addr);

/* Form the request. */
@@ -842,6 +870,18 @@ ftp_lpsv (int csock, ip_address *addr, int *port)
DEBUGP (("*port is: %d\n", *port));
}

+ /* Reject the response if the advertised address does not match the
+ control connection's peer. */
+ if (peer_addr.family != addr->family
+ || memcmp (IP_INADDR_DATA (addr), IP_INADDR_DATA (&peer_addr),
+ af == 4 ? 4 : 16) != 0)
+ {
+ xzero (*addr);
+ *port = 0;
+ xfree (respline);
+ return FTPINVPASV;
+ }
+
xfree (respline);
return FTPOK;
}
--
GitLab

7 changes: 6 additions & 1 deletion packages/wget/build.ncl
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ let version = "1.25.0" in
extract = true,
strip_prefix = "wget-%{version}",
} | Source,
# Security backports for the four CVEs upstream fixed after 1.25.0 (the last
# Security backports for the five CVEs upstream fixed after 1.25.0 (the last
# release, 2024-11-11). Each is a self-contained fix touching one src/ file
# and a different file from the others, so they apply independently and in
# any order. Cherry-picked from gitlab.com/gnuwget/wget; verified to apply
Expand All @@ -31,6 +31,11 @@ let version = "1.25.0" in
{ file = "CVE-2026-58470.patch" } | Local,
{ file = "CVE-2026-58471.patch" } | Local,
{ file = "CVE-2026-58472.patch" } | Local,
# CVE-2026-15146: PASV/LPSV reply address not checked against the control
# connection's peer, so a malicious FTP server (or an HTTP redirect to one)
# can point the data connection anywhere (SSRF). Upstream 4f85853f6418
# (2026-07-05); Debian carries it as 1.25.0-3.
{ file = "CVE-2026-15146.patch" } | Local,
base,
toolchain,
make,
Expand Down
54 changes: 54 additions & 0 deletions packages/wget/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ patch -Np1 -i "CVE-2026-58469.patch" # metalink.c clean_metalink_string buffe
patch -Np1 -i "CVE-2026-58470.patch" # http.c parse_content_range integer overflow
patch -Np1 -i "CVE-2026-58471.patch" # url.c filename-conversion buffer size
patch -Np1 -i "CVE-2026-58472.patch" # convert.c html_quote_string integer+buffer overflow
patch -Np1 -i "CVE-2026-15146.patch" # ftp-basic.c PASV/LPSV address not checked against peer (SSRF)

case $(uname -m) in
x86_64) MARCH="-march=x86-64-v3" ;;
Expand All @@ -24,4 +25,57 @@ export CXXFLAGS="${CFLAGS}"
--with-ssl=openssl
make -j$(nproc)

# Regression test for CVE-2026-15146. A fake FTP server, written in gawk (its
# /inet sockets mean no new build dep), answers PASV with 127.0.0.2, which is
# not the peer of the control connection (127.0.0.1). Patched wget refuses the
# reply ("Cannot parse PASV response.") and never contacts 127.0.0.2. Unpatched
# wget opens its data connection to whatever address the server names: the SSRF.
PORT=$((20000 + $$ % 20000))
cat > ftp-pasv-spoof.awk <<'AWK'
BEGIN {
srv = "/inet/tcp/" port "/0/0"
ORS = "\r\n"
for (;;) {
print "220 fake ftp" |& srv; fflush(srv)
while ((srv |& getline line) > 0) {
sub(/\r$/, "", line)
cmd = toupper(substr(line, 1, 4))
if (cmd == "USER") reply = "331 password please"
else if (cmd == "PASS") reply = "230 logged in"
else if (cmd == "SYST") reply = "215 UNIX Type: L8"
else if (cmd == "PWD") reply = "257 \"/\""
else if (cmd == "TYPE") reply = "200 ok"
else if (cmd == "CWD ") reply = "250 ok"
else if (cmd == "SIZE") reply = "213 4"
else if (cmd == "PASV") reply = "227 Entering Passive Mode (127,0,0,2,0,9)"
else if (cmd == "QUIT") reply = "221 bye"
else reply = "502 not implemented"
print reply |& srv; fflush(srv)
if (cmd == "QUIT") break
}
close(srv)
}
}
AWK
gawk -v port="$PORT" -f ftp-pasv-spoof.awk &
FAKE_FTP=$!
# Wait until it listens. The probe connection is answered and dropped, and the
# server loops back to accept the next one.
i=0
until bash -c "exec 3<>/dev/tcp/127.0.0.1/$PORT" 2>/dev/null; do
i=$((i + 1))
[ "$i" -lt 100 ] || { echo "fake FTP server never listened"; exit 1; }
sleep 0.1
done
timeout 60 ./src/wget --no-config --no-proxy --tries=1 --passive-ftp \
-O /dev/null "ftp://127.0.0.1:$PORT/file" > ftp-pasv-spoof.log 2>&1 || true
kill "$FAKE_FTP" 2>/dev/null || true
cat ftp-pasv-spoof.log
grep -q "Cannot parse PASV response" ftp-pasv-spoof.log
if grep -q "127\.0\.0\.2" ftp-pasv-spoof.log; then
echo "CVE-2026-15146 regression: wget followed a spoofed PASV address"
exit 1
fi
rm -f ftp-pasv-spoof.awk ftp-pasv-spoof.log

make DESTDIR="$OUTPUT_DIR" install
Loading
0