#22865·curl

IMAP custom FETCH listing classification skips literal boundaries, enabling response desynchronization

Author: bagderCreated Sep 7, 2026Updated Sep 8, 2026
LabelsIMAP

I did this

(Issue originally submitted to us by Stanislav Fort as a potential security on Hackerone. We do not consider it a vulnerability, but a bug - thus submitted here)

is_custom_fetch_listing_match() in lib/imap.c classifies any custom FETCH whose sequence set contains : or , as a "listing." For that classification, imap_state_listsearch_resp() skips literal detection entirely. The ping-pong line parser then scans the server-declared literal body as IMAP protocol lines. A tagged completion line embedded inside attacker-controlled message content terminates the active request prematurely, and the remaining literal bytes are interpreted as responses to a subsequent request on the reused connection.

The result is that content from one fetched message can be returned as the body of a different requested message, while the genuine response is suppressed.

Root cause

is_custom_fetch_listing_match() at lib/imap.c:1181-1197:

c
static bool is_custom_fetch_listing_match(const char *params)
{
  if(*params++ != ' ')
    return FALSE;
  while(ISDIGIT(*params)) {
    params++;
    if(*params == 0)
      return FALSE;
  }
  if(*params == ':')
    return TRUE;
  if(*params == ',')
    return TRUE;
  return FALSE;
}

This classifies UID FETCH 1:* BODY[] as a listing purely because of the : in the sequence set, without considering the requested data items.

imap_state_listsearch_resp() at lib/imap.c:1233-1235:

c
if(imapcode == '*' && is_custom_fetch_listing(imap)) {
  /* custom FETCH or UID FETCH for listing is not handled here */
}

This branch prevents the else if(imapcode == '*') block from executing. That block is the one that calls imap_find_literal(), parses {size}, and sets up proper literal consumption. With it skipped, Curl_pp_readresp() in lib/pingpong.c processes every \r\n-terminated line inside the literal as a protocol response. Any line matching the active tag terminates the request.

Reproduction

From a curl source checkout at or after commit c2676bf:

2. Run

Place client.c, server.py, and Dockerfile directory inside the curl source tree, then:

bash
docker build -t curl-imap-desync -f poc/Dockerfile .
docker run --rm curl-imap-desync

3. Expected output

MODE=vulnerable
FIRST_RESULT=0
FIRST_BYTES=0
SECOND_RESULT=0
SECOND_BODY=FORGED-FOR-UID-456
VERDICT=VULNERABLE
MODE=control
FIRST_RESULT=0
FIRST_BYTES=266361
SECOND_RESULT=0
SECOND_BODY=GENUINE-FOR-UID-456
VERDICT=CONTROL_OK

The vulnerable case shows SECOND_BODY=FORGED-FOR-UID-456 — the attacker's payload was returned as the body of the second, unrelated FETCH. The matched control changes only the first request from UID FETCH 1:* BODY[] to UID FETCH 1 BODY[]. With the single-UID form, curl honors the literal boundary and returns GENUINE-FOR-UID-456.

The server withholds the forged FETCH response until after curl has sent the second command, proving the substitution happens through connection reuse rather than buffered data.

Impact

An attacker who can place a message in the target's IMAP mailbox (i.e., send an email) can embed a ~266 KB spray of fake tagged completion lines covering all 26 tag prefixes and all 256 command-counter values. When an application fetches this message as part of a multi-UID custom FETCH (e.g., UID FETCH 1:* BODY[]) and then reuses the connection for a second request, curl returns the attacker-controlled payload as the body of the second request, suppressing the server's genuine response.

Demonstrated impact is integrity and availability. I have not demonstrated confidentiality loss, command injection, memory corruption, or code execution.

Exploitation requires an application to:

  1. issue a custom multi-message content FETCH containing : or , via CURLOPT_CUSTOMREQUEST;
  2. include an attacker-controlled message in that FETCH;
  3. reuse the same authenticated IMAP connection for a subsequent request; and
  4. trust libcurl's association between requests and returned message bodies.

This issue was introduced in commit e788d9d2c76b8b28ef0ba5

/cc @MonkeybreadSoftware

I expected the following

see above

curl/libcurl version

git master

operating system

independent