✓ProveIt
Examples / Report #3972403 Saved analysis

CVE-2026-87864: IMAP APPEND plus CRLF conversion command injection

curl · 2026-08-2613.9s analysis · $0.019 in model calls

What the report claims

When CURLOPT_CRLF is enabled, curl's IMAP APPEND command declares an incorrect byte count in the literal, causing overflow bytes to be parsed as new IMAP commands.

Compare with curl's recorded outcome

Known vulnerability

A real vulnerability: CVE-2026-87864 IMAP APPEND plus CRLF conversion command injection

Shown for comparison after analysis. The pipeline did not see this label.

ProveIt's finding

Code supports the claim

The code supports the reported flaw. A maintainer should review its impact and decide what to do next.

Why this finding?
  • imap_perform_append sends original file size in APPEND {N} literal (lib/imap.c:943-944)
  • Reader stack is initialized with original size (lib/imap.c:894, lib/sendf.c:1156)
  • CRLF conversion reader expands LF→CRLF but reports unknown total length (-1), so converted size is never computed
  • Curl_req_send_more transmits until EOS with no check against declared literal size (lib/request.c:458-479)
  • Attacker controlling upload content can place IMAP commands after the boundary; they execute in authenticated session
  • No bounds check, pre-computation, or rejection of the CURLOPT_CRLF+APPEND combination exists in provided code
How this analysis ran 5 claims · 11 citations verified

Checked against curl tested commit c2676bf9e639 (c2676bf9e639). Test evaluation split. Citation verification checks that a referenced line was in the evidence; it does not prove the model interpreted it correctly.

1 · Grounding
grounded
no model · 14 references checked out, 0 did not
2 · Claims
NVIDIA-Nemotron-3-Nano-30B-A3B
5 claims · 2403 tokens · $0.0003
3 · Evidence
5 code snippets
real curl source at c2676bf9e639 · 3 docs via Tavily
4 · Judgment
Nemotron-3-Ultra-550b-a55b
12587 tokens · 10.5s · $0.0190
5 · Receipts
11 of 12 citations verified
every citation must point at code the judge was shown
Review a draft reply to the reporter

Edit and verify before sending.

Thank you for the detailed report and reproduction. The code confirms the framing mismatch: imap_perform_append declares the original size in the APPEND literal (lib/imap.c:943) while the send loop (lib/request.c:458) transmits the CRLF-expanded body without stopping at that boundary. This allows command injection in the authenticated IMAP session. We will treat this as a security issue and evaluate the suggested fixes (reject the combination or pre-compute converted size).

Inspect all claims and source code 5 claims · 5 code excerpts

Report claims

C1flaw supported

curl's IMAP APPEND command declares the original input file size in the {N} literal but transmits more bytes due to CRLF expansion

Read the reasoning

The code shows imap_perform_append sends the original file size in the IMAP APPEND {N} literal (line 943-944) while setting up the reader with that same original size (line 894). When CRLF conversion is enabled, a line-conversion reader (cr-lineconv) is stacked on top, which expands bare LF to CRLF, causing more bytes to be transmitted than declared.

C2location supported

in lib/imap.c, imap_perform_append() sets up the upload reader with the original size

Read the reasoning

Directly visible in imap_perform_append at line 894.

C3trigger supported

enabling CURLOPT_CRLF or CURLOPT_TRANSFERTEXT=1 during an authenticated IMAP APPEND upload

Read the reasoning

The deterministic checks confirm both options exist. CURLOPT_CRLF / CURLOPT_TRANSFERTEXT=1 triggers the line-conversion reader stack in the creader layer (not shown in provided snippets but confirmed by symbol existence and curl architecture).

lib/imap.c:894 deterministic check
C4impact supported

overflow bytes are parsed as new IMAP commands in the same authenticated session

Read the reasoning

Curl_req_send_more (the send loop) has no awareness of the IMAP literal size; it transmits until the reader signals EOS. The server, per IMAP protocol (RFC 3501), consumes exactly the declared N bytes as the literal body, then parses subsequent bytes as new commands in the same authenticated session.

C5reproduction supported

upload a 66-byte file with 22 bare LFs followed by a suffix, enable --crlf, and perform IMAP APPEND; the server consumes only the declared size and parses the remaining bytes as commands

Read the reasoning

The reproduction describes a concrete instance of the general mechanism: 22 bare LFs expand by 22 bytes, pushing attacker-controlled suffix past the declared boundary. The code path supports this exactly.

What the report says about other projects

not addressed The server consumes exactly N bytes as the literal, and the overflow bytes are parsed as new IMAP commands in the same authenticated session.

Read the original report (excerpt)

Full report on HackerOne ↗

## Summary When `CURLOPT_CRLF` is enabled, `imap_perform_append()` advertises the **original** input file size in the IMAP `APPEND ... {N}` literal but then transmits the **expanded** body (bare LF → CRLF). The server consumes exactly N bytes as the literal, and the overflow bytes are parsed as new IMAP commands in the same authenticated session. This is a protocol framing bug — curl tells the server "here come N bytes," then sends more than N — not a case of the user injecting CRLFs into a protocol field. The bytes originate in the upload body, and curl's own conversion moves them across the literal boundary. ## Affected versions Tested and confirmed on commit `c2676bf9e639e31e44b12a2cc82cf2046231b056` (curl 8.22.0-DEV) and curl 8.6.0. The mismatch likely dates back to IMAP APPEND's introduction in 7.30.0. ## Root cause In `lib/imap.c`, `imap_perform_append()` sets up the upload reader with the original size and sends it in the IMAP literal: ```c result = Curl_creader_set_fread(data, data->state.infilesize); ... result = imap_sendf(data, imapc, "APPEND %s%s {%" FMT_OFF_T "}", mailbox, curlx_dyn_ptr(&flags), data->state.infilesize); ``` The `cr-lineconv` reader (`lib/sendf.c`) converts each bare LF to CRLF and explicitly returns `-1` for its total length because the converted size is unknown. `Curl_req_send_more()` (`lib/request.c`) drains the reader until EOS — it does not stop at the IMAP-declared boundary. An N-byte input containing k bare LFs produces N+k wire bytes while the server was told to consume only N. ## Reproduction 1. Build curl at commit c2676bf9e639e31e44b12a2cc82cf2046231b056 (or any version from 7.30.0 onward) on a Linux or macOS host, without TLS (plain IMAP is sufficient for reproduction). 2. Create a 66-byte upload file consisting of A\n repeated 22 times (44 bytes), followed by the 22-byte suffix \nX CREATE "Injected"\r\n. The 22 bare LFs before the suffix are chosen so that CRLF expansion adds exactly 22 bytes — pushing the suffix past the declared literal boundary. 3. Start a local IMAP server on port 11430 that implements standard IMAP framing: respond to CAPABILITY, LOGIN, and APPEND. On APPEND, parse the {N} literal size from the command, send + continue, then consume exactly N bytes from the socket as the literal body. After consuming the literal and its terminating CRLF, read and record the next l …

Source code

lib/imap.clines 853–954 · definition of imap_perform_append
853static CURLcode imap_perform_append(struct Curl_easy *data,
854 struct imap_conn *imapc,
855 struct IMAP *imap)
856{
857 CURLcode result = CURLE_OK;
858 char *mailbox;
859 struct dynbuf flags;
860
861 /* Check we have a mailbox */
862 if(!imap->mailbox) {
863 failf(data, "Cannot APPEND without a mailbox.");
864 return CURLE_URL_MALFORMAT;
865 }
866
867#ifndef CURL_DISABLE_MIME
868 /* Prepare the mime data if some. */
869 if(IS_MIME_POST(data)) {
870 curl_mimepart *postp = data->set.mimepostp;
871
872 /* Use the whole structure as data. */
873 postp->flags &= ~(unsigned int)MIME_BODY_ONLY;
874
875 /* Add external headers and mime version. */
876 curl_mime_headers(postp, data->set.headers, 0);
877 result = Curl_mime_prepare_headers(data, postp, NULL,
878 NULL, MIMESTRATEGY_MAIL);
879
880 if(!result)
881 if(!Curl_checkheaders(data, STRCONST("Mime-Version")))
882 result = Curl_mime_add_header(&postp->curlheaders,
883 "Mime-Version: 1.0");
884
885 if(!result)
886 result = Curl_creader_set_mime(data, postp);
887 if(result)
888 return result;
889 data->state.infilesize = Curl_creader_client_length(data);
890 }
891 else
892#endif
893 {
894 result = Curl_creader_set_fread(data, data->state.infilesize);
C1: Curl_creader_set_fread called with original infilesize
C2: Curl_creader_set_fread(data, data->state.infilesize) sets up reader with original size
C3: Reader setup path used for non-MIME uploads
895 if(result)
896 return result;
897 }
898
899 /* Check we know the size of the upload */
900 if(data->state.infilesize < 0) {
C2: Explicit check that infilesize >= 0, confirming known size is required
901 failf(data, "Cannot APPEND with unknown input file size");
902 return CURLE_UPLOAD_FAILED;
903 }
904
905 /* Make sure the mailbox is in the correct atom format */
906 mailbox = imap_atom(imap->mailbox, FALSE);
907 if(!mailbox)
908 return CURLE_OUT_OF_MEMORY;
909
910 /* Generate flags string and send the APPEND command */
911 curlx_dyn_init(&flags, 100);
912 if(data->set.upload_flags) {
913 int i;
914 struct ulbits ulflag[] = {
915 { CURLULFLAG_ANSWERED, "Answered" },
916 { CURLULFLAG_DELETED, "Deleted" },
917 { CURLULFLAG_DRAFT, "Draft" },
918 { CURLULFLAG_FLAGGED, "Flagged" },
919 { CURLULFLAG_SEEN, "Seen" },
920 { 0, NULL }
921 };
922
923 result = CURLE_OUT_OF_MEMORY;
924 if(curlx_dyn_add(&flags, " (")) {
925 goto cleanup;
926 }
927
928 for(i = 0; ulflag[i].bit; i++) {
929 if(data->set.upload_flags & ulflag[i].bit) {
930 if((curlx_dyn_len(&flags) > 2 && curlx_dyn_add(&flags, " ")) ||
931 curlx_dyn_add(&flags, "\\") ||
932 curlx_dyn_add(&flags, ulflag[i].flag))
933 goto cleanup;
934 }
935 }
936
937 if(curlx_dyn_add(&flags, ")"))
938 goto cleanup;
939 }
940 else if(curlx_dyn_add(&flags, ""))
941 goto cleanup;
942
943 result = imap_sendf(data, imapc, "APPEND %s%s {%" FMT_OFF_T "}",
C1: imap_sendf called with data->state.infilesize as the literal size in APPEND command
C5: APPEND literal uses original infilesize
944 mailbox, curlx_dyn_ptr(&flags), data->state.infilesize);
945
946cleanup:
947 curlx_dyn_free(&flags);
948 curlx_free(mailbox);
949
950 if(!result)
951 imap_state(data, imapc, IMAP_APPEND);
952
953 return result;
954}
lib/sendf.clines 1146–1164 · definition of Curl_creader_set_fread
1146CURLcode Curl_creader_set_fread(struct Curl_easy *data, curl_off_t len)
1147{
1148 CURLcode result;
1149 struct Curl_creader *r;
1150 struct cr_in_ctx *ctx;
1151
1152 result = Curl_creader_create(&r, data, &cr_in, CURL_CR_CLIENT);
1153 if(result || !r)
1154 goto out;
1155 ctx = r->ctx;
1156 ctx->total_len = len;
C1: creader context stores original len as total_len
C5: Reader tracks original length, not converted length
1157
1158 cl_reset_reader(data);
1159 result = do_init_reader_stack(data, r);
1160out:
1161 CURL_TRC_READ(data, "add fread reader, len=%" FMT_OFF_T " -> %d",
1162 len, (int)result);
1163 return result;
1164}
lib/request.clines 458–479 · definition of Curl_req_send_more
458CURLcode Curl_req_send_more(struct Curl_easy *data)
C4: Curl_req_send_more drains reader until eos_read without reference to declared literal size
459{
460 CURLcode result;
461
462 /* Fill our send buffer if more from client can be read. */
463 if(!data->req.upload_aborted &&
464 !data->req.eos_read &&
465 !Curl_xfer_send_is_paused(data) &&
466 !Curl_bufq_is_full(&data->req.sendbuf)) {
467 size_t nread;
468 result = Curl_bufq_sipn(&data->req.sendbuf, 0,
C4: Loop continues while !data->req.eos_read and sendbuf not full
C5: Send loop ignores literal boundary
469 add_from_client, data, &nread);
470 if(result && result != CURLE_AGAIN)
471 return result;
472 }
473
474 result = req_flush(data);
475 if(result == CURLE_AGAIN)
476 result = CURLE_OK;
477
478 return result;
479}
lib/imap.clines 156–191 · definition of imap_sendf
Show 36 lines (not cited by the judge)
156static CURLcode imap_sendf(struct Curl_easy *data,
157 struct imap_conn *imapc,
158 const char *fmt, ...) CURL_PRINTF(3, 0);
159static CURLcode imap_sendf(struct Curl_easy *data,
160 struct imap_conn *imapc,
161 const char *fmt, ...)
162{
163 CURLcode result = CURLE_OK;
164
165 DEBUGASSERT(fmt);
166
167 /* Calculate the tag based on the connection ID and command ID */
168 curl_msnprintf(imapc->resptag, sizeof(imapc->resptag), "%c%03d",
169 'A' + curlx_sltosi((long)(data->conn->connection_id % 26)),
170 ++imapc->cmdid);
171
172 /* start with a blank buffer */
173 curlx_dyn_reset(&imapc->dyn);
174
175 /* append tag + space + fmt */
176 result = curlx_dyn_addf(&imapc->dyn, "%s %s", imapc->resptag, fmt);
177 if(!result) {
178 va_list ap;
179 va_start(ap, fmt);
180#ifdef __clang__
181#pragma clang diagnostic push
182#pragma clang diagnostic ignored "-Wformat-nonliteral"
183#endif
184 result = Curl_pp_vsendf(data, &imapc->pp, curlx_dyn_ptr(&imapc->dyn), ap);
185#ifdef __clang__
186#pragma clang diagnostic pop
187#endif
188 va_end(ap);
189 }
190 return result;
191}
lib/curlx/dynbuf.clines 237–243 · definition of curlx_dyn_ptr
Show 7 lines (not cited by the judge)
237char *curlx_dyn_ptr(const struct dynbuf *s)
238{
239 DEBUGASSERT(s);
240 DEBUGASSERT(s->init == DYNINIT);
241 DEBUGASSERT(!s->leng || s->bufr);
242 return s->bufr;
243}