HexBrain

curl · Report #4123474

Cross-protocol redirect to imap:// or smtp:// uploads the -F/MIMEPOST form and the custom Authorization/Cookie headers to the redirect target

N/Astatus
lowseverity
1upvotes
26 Oct 2026disclosed

How it was found

Summary:

When a transfer that posts a multipart form (CURLOPT_MIMEPOST, the tool's -F) follows a redirect to an imap:// URL, curl APPENDs the whole form to the mailbox on the redirect target and puts the application's CURLOPT_HTTPHEADER list into the mail headers, including custom Authorization: and Cookie: headers; CURLOPT_HTTPHEADER(3) states that since 7.58.0 and 7.64.0 libcurl "specifically prevents" these two "from being sent to other hosts than the first used one", and lib/http.c:1922-1927 enforces that for HTTP, but imap_perform_append() passes data->set.headers to curl_mime_headers() with no such check (lib/imap.c:876 in 8.23.0, :861 in master). It also happens on 301, 302 and 303, where HTTP itself sends no body at all: follow_switch_get() calls http_switch_to_get() for those three codes (http.c:1329, :1351, :1361), which sets data->state.httpreq = HTTPREQ_GET (:1171), but IMAP decides to APPEND from IS_MIME_POST() (imap.c:2080, :2018), which only tests that a MIME post is configured (urldata.h:1142), not what the current request is. The same mismatch means an application that reuses a handle after a MIME post and then sets CURLOPT_HTTPGET for an unrelated request uploads the earlier form, plus its Authorization header, to an IMAP URL that later request is redirected to: CURLOPT_HTTPGET(3) is documented for precisely that, to "get back to using GET" when "a POST, HEAD, PUT, etc has been used previously using the same curl handle", and promises that libcurl then "automatically sets CURLOPT_NOBODY(3) to 0 and CURLOPT_UPLOAD(3) to 0", but it does not clear the MIME post (setopt.c:473-481), so IS_MIME_POST() stays true and an application that followed that documentation still uploads. With the same commands a redirect to http:// sends a plain GET with no body and neither header on 301/302/303, and on 307 sends the body but still withholds those two headers unless --location-trusted (CURLOPT_UNRESTRICTED_AUTH) is given, which is the opt-in the same documentation names; on the IMAP path that option changes nothing either way, because no such check is ever reached. lib/smtp.c has the identical code (:1008-1015, :1797) and leaks the same way once CURLOPT_MAIL_RCPT is set, which the attached run confirms.

Affected version

Uploading the form on a 301/302/303 GET downgrade exists since IMAP MIME upload arrived in 7.56.0 (ce0881edee); the Authorization: leak dates from 7.58.0 and the Cookie: leak from 7.64.0, the releases that first withheld each of them cross-host. Up to 8.23.0 and master 59d2537b40 (imap.c:861, :2003, :2065 and urldata.h:1133 there). Both the curl tool and libcurl. Tested on macOS 27.0.1 arm64:

curl 8.23.0-DEV libcurl/8.23.0-DEV OpenSSL/3.6.4   curl-8_23_0, tool and static libcurl
curl 8.23.1-DEV libcurl/8.23.1-DEV OpenSSL/3.6.4   master 59d2537b40, tool and static libcurl
curl 8.22.0 ... AppleIDN ... (Homebrew, as shipped) tool and shared libcurl
curl 8.7.1 (SecureTransport) LibreSSL/3.3.6        macOS /usr/bin/curl, as shipped

Steps To Reproduce:

Everything runs on 127.0.0.1 with Python 3 servers from the attached zip (srv.py redirects, imapsink.py and smtpsink.py are the attacker's mail servers and log what arrives, sinksrv.py is the HTTP control target); host names are mapped with --resolve. In the unzipped poc/ directory, ./build.sh && ./run_tool.sh && ./run_lib.sh does steps 1 to 3 against the curl on PATH and Homebrew's libcurl.

  1. CURL=/path/to/curl ./run_tool.sh runs, for 301, 302, 303 and 307: curl -L --proto-redir =http,imap -F [email protected] -F note=hello -H 'Authorization: Bearer SECRET-TOKEN-123' -H 'Cookie: sid=SECRET-COOKIE-456' 'http://a.test:23201/x?code=<code>&loc=imap://b.test:23270/INBOX' then the same command redirected to http://b.test:23240/y as the control, the SMTP twin (--mail-rcpt, 302 to smtp://b.test:23280/), a direct imap:// upload as reference, and four OPT-IN CHECK runs repeating the HTTP and IMAP redirects with and without --location-trusted. Each case prints a => line saying whether Authorization, Cookie and the file content arrived.
  2. ./build.sh [curl build dir] builds poc_mime_imap.c; without an argument it links Homebrew's libcurl.
  3. ./run_lib.sh runs it: prior does a CURLOPT_MIMEPOST to the application's own server with a custom Authorization header, then sets CURLOPT_HTTPGET=1 for another URL on the same handle which answers 302 Location: imap://b.test/INBOX; noprior is the same without the earlier form.

Results (evidence/01 for 8.23.0, 02 for master, 03 for the reused handle, 05 for the stock binaries; all four builds above identical):

                                   301        302        303        307
-F form to imap://   APPENDed      yes        yes        yes        yes      Authorization + Cookie + file content
CONTROL, to http://  sent          GET only   GET only   GET only   body     no Authorization, no Cookie
SMTP twin (302, --mail-rcpt)       mail DATA with Authorization + Cookie + file content
libcurl reused handle, HTTPGET     APPEND of the earlier form + Authorization   (noprior control: LIST only)
REFERENCE direct imap:// upload    APPEND with its own -H headers (documented behaviour, unchanged)

OPT-IN CHECK                       default              --location-trusted
  http://b.test/ 307               no Authorization/Cookie   both sent      <- the documented switch works
  imap://b.test/ 302               both sent                 both sent      <- same either way: no check

Fix

fix/imap-smtp-mime-on-redirect.diff (applies to master 59d2537b40 and to curl-8_23_0): IMAP and SMTP upload a MIME post only when the current request is one (data->state.httpreq == HTTPREQ_POST_MIME), and the mail headers taken from CURLOPT_HTTPHEADER leave out Authorization: and Cookie: when Curl_auth_allowed_to_host() is false — the helper curl documents as deciding whether authentication, cookies or other sensitive data "can be sent to the connection's origin" (lib/vauth/vauth.c:137-138), and which already honours CURLOPT_UNRESTRICTED_AUTH, so this applies curl's existing policy to IMAP and SMTP instead of inventing a new one. Built and re-run (evidence/04): 301/302/303 end with a LIST and no upload, 307 uploads the body without those two headers exactly as the HTTP control does, the SMTP case stops at VRFY, the reused handle uploads nothing, the direct imap:// upload still carries its -H headers, and with --location-trusted the 307 upload carries them again, so the documented opt-in keeps working on IMAP just as it does on HTTP.

AI disclosure: I used an AI assistant (Claude) for code review, test setup and drafting. I reproduced every result myself with the attached scripts on the builds listed above; the logs in the zip are the raw output.

Impact

A server that a user or application posts a form to, anyone who can answer its plaintext HTTP, or an open redirect on it, can collect the bearer token or session cookie carried in custom Authorization: or Cookie: headers together with every file and field of the form, by answering with a redirect to an IMAP or SMTP server it controls. Those two headers are the ones curl documents as never being sent to another host, and the form is uploaded even on 301, 302 and 303, where curl deliberately sends no body at all on an HTTP redirect, so a GET-downgrade redirect that an application author would reasonably treat as harmless becomes an upload of the form to the attacker. A libcurl application that reuses a handle, as the documentation recommends, sends an earlier and completely unrelated form the same way on a later plain GET. The precondition is the one CVE-2025-14524 (bearer token on a cross-protocol redirect to IMAP, LDAP, POP3 or SMTP) also required and which curl rated Low: the transfer must follow redirects and allow imap or smtp there, which is not the default for either the tool or libcurl, so this needs a user or application that opted into those redirect protocols — that advisory calls its own precondition "A highly unusual combination" and curl still assigned a CVE at Low; --disallow-username-in-url does not help here, since no userinfo is involved. Within that precondition nothing else is needed, no authentication options and no attacker-side validity: it affects the curl tool as well as libcurl, every version since 7.56.0 including macOS's and Homebrew's stock binaries, and the leak is exactly what CVE-2018-1000007 and CVE-2022-27776 fixed for HTTP.

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