CVE-2026-45232
Description
Rsync versions before 3.4.3 contain an off-by-one out-of-bounds stack write vulnerability in the establish_proxy_connection() function in socket.c that allows network attackers to corrupt stack memory by sending a malformed HTTP proxy response. Attackers can exploit this by positioning themselves between the client and proxy or controlling the proxy server to send a response line of 1023 or more bytes without a newline terminator, causing a null byte to be written to an out-of-bounds stack address when the RSYNC_PROXY environment variable is set.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Rsync versions before 3.4.3 contain an off-by-one stack OOB write in establish_proxy_connection() that can corrupt stack memory when a malicious proxy sends a long response line without a newline.
Vulnerability
Rsync versions before 3.4.3 contain an off-by-one out-of-bounds stack write vulnerability in the establish_proxy_connection() function in socket.c. The client reads the HTTP proxy's first response line one byte at a time into a 1024-byte stack buffer with the bound cp < &buffer[sizeof buffer - 1], so the loop only ever writes buffer[0..sizeof-2]. If the proxy (or a man-in-the-middle in front of it) returns 1023+ bytes on the first response line without a \n terminator, the loop exits with cp == &buffer[sizeof buffer - 1] — a slot the loop never wrote, so *cp holds stale stack bytes left there by the earlier snprintf() that formatted the outgoing CONNECT request. The post-loop code then checks if (*cp != '\n') cp++; and then *cp-- = '\0';, which writes a null byte one byte past the end of the on-stack buffer, corrupting the adjacent stack slot. The vulnerability is reachable only when the RSYNC_PROXY environment variable is set, directing rsync to tunnel an rsync:// connection through an HTTP CONNECT proxy. [1][2]
Exploitation
An attacker must be able to control the HTTP proxy response sent to the rsync client. This can be achieved by positioning themselves as a man-in-the-middle between the client and the proxy, or by controlling the proxy server itself. The attacker sends an HTTP CONNECT response with a status line of 1023 or more bytes that does not contain a newline terminator. When the rsync client processes this response with RSYNC_PROXY set, the bug triggers and a null byte is written one byte past the end of the 1024-byte stack buffer. No authentication or special prerequisite is required from the attacker beyond network access to inject the malicious response. The user must have configured the RSYNC_PROXY environment variable for the vulnerable code path to execute. [1][2]
Impact
The written byte is always \0 and the offset is fixed by the buffer size, not attacker-chosen, so this is not an arbitrary-write primitive. Practical impact is corruption of one adjacent stack byte in the rsync client process, potentially leading to misbehaviour or a crash (denial of service). No information disclosure beyond what the existing rprintf(FERROR, "bad response from proxy -- %s\n", buffer) path might print, and no server-side exposure. The CVSS v3 base score is 3.1 (Low), with the vector AV:N/AC:H/PR:N/UI:R/S:U/C:N/I:N/A:L. [1][2]
Mitigation
The vulnerability is fixed in rsync version 3.4.3, released on 2026-05-20 [3]. The fix detects the "buffer filled without newline" condition and avoids the out-of-bounds write [1]. Users should upgrade to version 3.4.3 or later. For those unable to upgrade, avoiding the use of the RSYNC_PROXY environment variable eliminates the attack surface. No workaround is available for scenarios that require HTTP CONNECT proxy support. The CVE is not listed on CISA's Known Exploited Vulnerabilities catalog. [1][3]
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
2- Range: <3.4.3
Patches
2a5fc5ebe7a8esocket: reject over-long proxy response line
2 files changed · +145 −13
socket.c+17 −13 modified@@ -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");
testsuite/proxy-response-line-too-long.test+128 −0 added@@ -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
32b9a8e989f6Vulnerability mechanics
Root cause
"Off-by-one error in establish_proxy_connection() in socket.c: when the proxy response line fills the entire 1024-byte buffer without a newline, the null-termination writes one byte past the end of the stack buffer."
Attack vector
An attacker who can control or man-in-the-middle the HTTP proxy (when RSYNC_PROXY is set) sends a first response line of exactly 1023 or more bytes without a newline terminator. The read loop in establish_proxy_connection() fills buffer[0..1022] and leaves cp at &buffer[1023]; the post-loop logic then writes a null byte at buffer[1024], which is one byte past the end of the stack array [CWE-193]. The attacker does not control the value written (always '\0') nor the exact offset, so the impact is limited to corruption of one adjacent stack byte, potentially causing a crash or misbehaviour.
Affected code
The vulnerability is in the `establish_proxy_connection()` function in `socket.c`. The stack buffer `buffer[1024]` is filled byte-by-byte in a loop that stops at `&buffer[sizeof buffer - 1]` (index 1023). When no newline is received, the post-loop code unconditionally does `cp++; *cp-- = '\0'`, writing a null byte at buffer[1024] — one byte past the array boundary.
What the fix does
The patch [patch_id=893724] introduces a PROXY_BUF_SIZE constant (1024) and changes the buffer declaration to `buffer[PROXY_BUF_SIZE + 1]` to reserve space for the null terminator. After the read loop, the code now checks whether cp reached the last valid position (`&buffer[PROXY_BUF_SIZE - 1]`); if so, it prints "proxy response line too long" and returns -1 instead of writing past the end. The null-termination logic is also corrected to write `*cp = '\0'` at the current position and adjust for a trailing '\r' using `cp[-1]`, eliminating the out-of-bounds write entirely.
Preconditions
- configThe RSYNC_PROXY environment variable must be set, directing rsync to use an HTTP proxy.
- networkThe attacker must be able to control the proxy server or perform a man-in-the-middle attack between the client and the proxy.
- inputThe proxy must send a first response line of 1023 or more bytes without a newline (' ') terminator.
Generated on May 20, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
3News mentions
0No linked articles in our index yet.