Package: release.debian.org
Control: affects -1 + src:rsync
X-Debbugs-Cc: [email protected]
User: [email protected]
Usertags: pu
Tags: bookworm
X-Debbugs-Cc: [email protected]
Severity: normal
[ Reason ]
This update fixes CVE-2026-45232, a minor/no-dsa client-side issue in
RSYNC_PROXY handling.
When rsync connects through an HTTP proxy using RSYNC_PROXY, an overlong
proxy response line could trigger a one-byte out-of-bounds stack write.
The write is a fixed NUL byte, so the practical impact is limited, but
the vulnerable code is present in bookworm.
[ Impact ]
Clients using RSYNC_PROXY could crash or misbehave when receiving an
overlong HTTP proxy response line from a malicious proxy or MITM.
[ Tests ]
The package built successfully in Salsa CI:
https://salsa.debian.org/aquila/rsync/-/pipelines/1098495
The targeted upstream regression test added by this patch also passed:
PASS proxy-response-line-too-long
See: https://salsa.debian.org/aquila/rsync/-/jobs/9693182#L3264
The change was also reviewed and approved on Salsa by Samuel Henrique,
one of the rsync maintainers.
[ Risks ]
Low. The patch is small, comes from upstream's v3.2.7 security patch
branch, and only rejects an invalid overlong HTTP proxy response line.
[ Checklist ]
[x] *all* changes are documented in the d/changelog
[x] I reviewed all changes and I approve them
[x] attach debdiff against the package in (old)stable
[x] the issue is verified as fixed in unstable
[ Changes ]
Import the upstream v3.2.7-sec-patches fix to reject overlong HTTP proxy
response lines.
No upstream version bump and no unrelated fixes are included.
diff -Nru rsync-3.2.7-1+deb12u5/debian/changelog
rsync-3.2.7-1+deb12u6/debian/changelog
--- rsync-3.2.7-1+deb12u5/debian/changelog 2026-05-20 02:10:17.000000000
-0400
+++ rsync-3.2.7-1+deb12u6/debian/changelog 2026-05-24 17:23:41.000000000
-0400
@@ -1,3 +1,12 @@
+rsync (3.2.7-1+deb12u6) bookworm; urgency=medium
+
+ * Non-maintainer upload.
+ * Import upstream patch to reject overlong HTTP proxy response lines,
+ avoiding a one byte out of bounds stack write when using RSYNC_PROXY.
+ (CVE-2026-45232).
+
+ -- Aquila Macedo Costa <[email protected]> Sun, 24 May 2026 18:23:41 -0300
+
rsync (3.2.7-1+deb12u5) bookworm-security; urgency=high
* Non-maintainer upload by the Security Team.
diff -Nru rsync-3.2.7-1+deb12u5/debian/patches/CVE-2026-45232.patch
rsync-3.2.7-1+deb12u6/debian/patches/CVE-2026-45232.patch
--- rsync-3.2.7-1+deb12u5/debian/patches/CVE-2026-45232.patch 1969-12-31
19:00:00.000000000 -0500
+++ rsync-3.2.7-1+deb12u6/debian/patches/CVE-2026-45232.patch 2026-05-24
17:23:41.000000000 -0400
@@ -0,0 +1,235 @@
+From: Andrew Tridgell <[email protected]>
+Date: Wed, 13 May 2026 20:35:35 +1000
+Subject: socket: reject over-long proxy response line
+
+fixes a one byte stack overflow when using RSYNC_PROXY with a
+malicious proxy.
+
+Reach: only when RSYNC_PROXY is set and a malicious or MITM'd
+proxy returns the pathological response. The byte written is
+always '\0' and the attacker doesn't choose the offset, so impact
+is corruption of one adjacent stack byte and possible later
+misbehaviour or crash -- no information disclosure beyond the
+existing rprintf of buffer contents.
+
+Reported by Aisle Research via Michal Ruprich
+
+(cherry picked from commit 36860669cceaa2ca7cc84367d1fb8a3655560300)
+---
+ socket.c | 30 ++++---
+ testsuite/proxy-response-line-too-long.test | 128 ++++++++++++++++++++++++++++
+ 2 files changed, 145 insertions(+), 13 deletions(-)
+ create mode 100755 testsuite/proxy-response-line-too-long.test
+
+diff --git a/socket.c b/socket.c
+index c2075ad..6a8f6f4 100644
+--- a/socket.c
++++ b/socket.c
+@@ -47,21 +47,23 @@ static struct sigaction sigact;
+
+ static int sock_exec(const char *prog);
+
++#define PROXY_BUF_SIZE 1024
++
+ /* Establish a proxy connection on an open socket to a web proxy by using the
+ * CONNECT method. If proxy_user and proxy_pass are not NULL, they are used
to
+ * authenticate to the proxy using the "Basic" proxy-authorization protocol.
*/
+ static int establish_proxy_connection(int fd, char *host, int port, char
*proxy_user, char *proxy_pass)
+ {
+- char *cp, buffer[1024];
+- char *authhdr, authbuf[1024];
++ char *cp, buffer[PROXY_BUF_SIZE + 1];
++ char *authhdr, authbuf[PROXY_BUF_SIZE + 1];
+ int len;
+
+ if (proxy_user && proxy_pass) {
+- stringjoin(buffer, sizeof buffer,
++ stringjoin(buffer, PROXY_BUF_SIZE,
+ proxy_user, ":", proxy_pass, NULL);
+ len = strlen(buffer);
+
+- if ((len*8 + 5) / 6 >= (int)sizeof authbuf - 3) {
++ if ((len*8 + 5) / 6 >= PROXY_BUF_SIZE - 3) {
+ rprintf(FERROR,
+ "authentication information is too long\n");
+ return -1;
+@@ -74,14 +76,14 @@ static int establish_proxy_connection(int fd, char *host,
int port, char *proxy_
+ authhdr = "";
+ }
+
+- len = snprintf(buffer, sizeof buffer, "CONNECT %s:%d
HTTP/1.0%s%s\r\n\r\n", host, port, authhdr, authbuf);
+- assert(len > 0 && len < (int)sizeof buffer);
++ len = snprintf(buffer, PROXY_BUF_SIZE, "CONNECT %s:%d
HTTP/1.0%s%s\r\n\r\n", host, port, authhdr, authbuf);
++ assert(len > 0 && len < PROXY_BUF_SIZE);
+ if (write(fd, buffer, len) != len) {
+ rsyserr(FERROR, errno, "failed to write to proxy");
+ return -1;
+ }
+
+- for (cp = buffer; cp < &buffer[sizeof buffer - 1]; cp++) {
++ for (cp = buffer; cp < &buffer[PROXY_BUF_SIZE - 1]; cp++) {
+ if (read(fd, cp, 1) != 1) {
+ rsyserr(FERROR, errno, "failed to read from proxy");
+ return -1;
+@@ -90,11 +92,13 @@ static int establish_proxy_connection(int fd, char *host,
int port, char *proxy_
+ break;
+ }
+
+- if (*cp != '\n')
+- cp++;
+- *cp-- = '\0';
+- if (*cp == '\r')
+- *cp = '\0';
++ if (cp == &buffer[PROXY_BUF_SIZE - 1]) {
++ rprintf(FERROR, "proxy response line too long\n");
++ return -1;
++ }
++ *cp = '\0';
++ if (cp > buffer && cp[-1] == '\r')
++ cp[-1] = '\0';
+ if (strncmp(buffer, "HTTP/", 5) != 0) {
+ rprintf(FERROR, "bad response from proxy -- %s\n",
+ buffer);
+@@ -110,7 +114,7 @@ static int establish_proxy_connection(int fd, char *host,
int port, char *proxy_
+ }
+ /* throw away the rest of the HTTP header */
+ while (1) {
+- for (cp = buffer; cp < &buffer[sizeof buffer - 1]; cp++) {
++ for (cp = buffer; cp < &buffer[PROXY_BUF_SIZE]; cp++) {
+ if (read(fd, cp, 1) != 1) {
+ rsyserr(FERROR, errno,
+ "failed to read from proxy");
+diff --git a/testsuite/proxy-response-line-too-long.test
b/testsuite/proxy-response-line-too-long.test
+new file mode 100755
+index 0000000..7f55c43
+--- /dev/null
++++ b/testsuite/proxy-response-line-too-long.test
+@@ -0,0 +1,128 @@
++#!/bin/sh
++
++# Copyright (C) 2026 by Andrew Tridgell
++
++# This program is distributable under the terms of the GNU GPL (see
++# COPYING).
++
++# Regression test for the off-by-one stack OOB write in
++# establish_proxy_connection() in socket.c when a malicious or
++# man-in-the-middle HTTP proxy returns a first response line of
++# 1023+ bytes without a '\n' terminator.
++#
++# Pre-fix: the read loop walked buffer[0..sizeof-2] one byte at a
++# time, then post-loop logic did "if (*cp != '\n') cp++; *cp-- =
++# '\0';". If no newline arrived before the loop filled the buffer,
++# cp was left at &buffer[sizeof-1] (never written by the loop),
++# *cp held stale stack bytes, and cp++ pushed cp one past the array.
++# The null-termination then wrote one byte out of bounds on the
++# stack. AddressSanitizer reports stack-buffer-overflow at the
++# null-termination site.
++#
++# Post-fix: the bound-exhaustion case is detected by position and
++# rejected with an "proxy response line too long" message, so no
++# OOB write occurs and rsync exits with a non-signal status.
++
++. "$suitedir/rsync.fns"
++
++command -v python3 >/dev/null 2>&1 || test_skipped "python3 not available"
++
++workdir="$scratchdir/workdir"
++mkdir -p "$workdir"
++cd "$workdir"
++
++port_file="$workdir/port"
++proxy_log="$workdir/proxy.log"
++
++# A minimal TCP listener: binds to an ephemeral port on 127.0.0.1,
++# writes the chosen port to $port_file *before* accept() so the test
++# can synchronise without a sleep, accepts one connection, reads
++# until end-of-headers or 64 KiB, sends exactly 1023 bytes of 'X'
++# with no '\n', then closes.
++python3 - "$port_file" >"$proxy_log" 2>&1 <<'PYEOF' &
++import socket, sys, os
++port_file = sys.argv[1]
++s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
++s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
++s.bind(("127.0.0.1", 0))
++port = s.getsockname()[1]
++tmp = port_file + ".tmp"
++with open(tmp, "w") as fp:
++ fp.write("%d\n" % port)
++os.rename(tmp, port_file) # atomic visibility to the shell side
++s.listen(1)
++conn, _ = s.accept()
++conn.settimeout(5)
++try:
++ data = b""
++ while b"\r\n\r\n" not in data and len(data) < 65536:
++ chunk = conn.recv(8192)
++ if not chunk:
++ break
++ data += chunk
++except socket.timeout:
++ pass
++conn.sendall(b"X" * 1023) # exactly the buffer-1 trigger size
++try:
++ conn.shutdown(socket.SHUT_RDWR)
++except OSError:
++ pass
++conn.close()
++s.close()
++PYEOF
++proxy_pid=$!
++
++# Wait up to ~10s for the listener to publish its port.
++i=0
++while [ ! -s "$port_file" ] && [ $i -lt 10 ]; do
++ sleep 1
++ i=$((i + 1))
++done
++
++if [ ! -s "$port_file" ]; then
++ kill "$proxy_pid" 2>/dev/null
++ cat "$proxy_log" >&2 2>/dev/null
++ test_fail "proxy listener never published a port"
++fi
++
++port=`cat "$port_file"`
++case "$port" in
++ *[!0-9]*|"") kill "$proxy_pid" 2>/dev/null; test_fail "bogus port from
listener: '$port'" ;;
++esac
++
++# Run rsync through the malicious proxy. Any rsync:// URL works:
++# the proxy intercepts the CONNECT and never forwards anywhere.
++rsync_err="$workdir/rsync.err"
++
++# rsync MUST exit non-zero here (the proxy is misbehaving).
++# Use `|| status=$?` so we capture the real exit code under `sh -e`;
++# `if ! cmd; then status=$?` would only ever see 0 because the `!`
++# is the last command before `$?`.
++status=0
++RSYNC_PROXY="127.0.0.1:$port" \
++ $RSYNC rsync://example.invalid:873/whatever/ "$workdir/out/" \
++ >/dev/null 2>"$rsync_err" || status=$?
++
++# Reap the listener.
++wait "$proxy_pid" 2>/dev/null || true
++
++# 1. rsync must not have crashed (SIGSEGV/SIGABRT report >= 128).
++if [ "$status" -ge 128 ]; then
++ cat "$rsync_err" >&2
++ test_fail "rsync killed by signal (status=$status) -- possible stack OOB
regression"
++fi
++
++# 2. rsync must have actually exited non-zero (i.e. saw the bad proxy).
++if [ "$status" -eq 0 ]; then
++ cat "$rsync_err" >&2
++ test_fail "rsync returned success despite malformed proxy response"
++fi
++
++# 3. The new error message must appear.
++if ! grep -q "proxy response line too long" "$rsync_err"; then
++ cat "$rsync_err" >&2
++ test_fail "expected 'proxy response line too long' in rsync stderr"
++fi
++
++echo "OK: over-long proxy response line rejected cleanly without crashing"
++exit 0
diff -Nru rsync-3.2.7-1+deb12u5/debian/patches/series
rsync-3.2.7-1+deb12u6/debian/patches/series
--- rsync-3.2.7-1+deb12u5/debian/patches/series 2026-05-20 01:48:49.000000000
-0400
+++ rsync-3.2.7-1+deb12u6/debian/patches/series 2026-05-24 17:23:41.000000000
-0400
@@ -61,3 +61,4 @@
# Regression from CVE-2024-12087 (#1093052)
Fix-FLAG_GOT_DIR_FLIST-collission-with-FLAG_HLINKED.patch
fix-aclocalm4-include-paths.patch
+CVE-2026-45232.patch