curl · Report #4123571
Redirect to imap/pop3/smtp (SASL) or sftp/scp (libssh) authenticates to the attacker's host with the user's ambient Kerberos credentials
How it was found
Summary:
When curl follows a redirect to an IMAP, POP3 or SMTP URL carrying any userinfo (imap://@attacker.example/ is enough), it authenticates to the redirect target with SASL GSSAPI out of the user's default Kerberos credential cache, although no credentials and no authentication method were configured. url_set_data_creds() gates option credentials on the origin (Curl_auth_allowed_to_origin(), lib/url.c:1376-1381) but merges the redirect URL's own userinfo in unconditionally as CREDS_URL (:1432-1433); that creds object alone makes Curl_sasl_can_authenticate() true (lib/curl_sasl.c:277); sasl_choose_krb5() then picks GSSAPI with no origin check at all (:315-341), because Curl_auth_user_contains_domain() treats an empty user name as valid under GSS-API or SSPI ("User and domain are obtained from the GSS-API credentials cache or the currently logged in user from Windows", lib/vauth/vauth.c:126-131), and the ticket is minted with GSS_C_NO_CREDENTIAL, the default ccache (lib/vauth/krb5_gssapi.c:140). The clearest way to see it is to compare two releases: on 8.22.0 curl --negotiate -u : -L redirected to http://:@b.test/ sends Authorization: Negotiate to that host and on 8.23.0 it sends none, because 557a52f21e (CVE-2026-94265) and 43029e2ec3 (CVE-2026-86413) fixed it, while the imap, pop3 and smtp rows below are identical on both. Those two fixes only touched lib/http.c and the HTTP connection matching in lib/url.c, although 557a52f21e states the intent as "Restrict Negotiate/NTLM to only "allowed" origins, irregardless of where credentials are sourced from"; neither the SASL code nor the libssh code calls Curl_auth_allowed_to_host() or Curl_auth_allowed_to_origin(), which is what the attached fix adds. On libssh builds a redirect to sftp:// or scp:// does the same over gssapi-with-mic and needs no userinfo: myssh_in_AUTHLIST() goes to SSH_AUTH_GSSAPI whenever the server offers it (lib/vssh/libssh.c:842-843), myssh_in_AUTH_GSSAPI() calls ssh_userauth_gssapi() (:928-947), and the only gate is CURLSSH_AUTH_GSSAPI, which the default CURLSSH_AUTH_ANY includes (url.c:375, curl.h:863-871). With CURLOPT_GSSAPI_DELEGATION / --delegation always, or GSSAPIDelegateCredentials yes in ssh_config for libssh, the forwarded token carries the user's TGT.
Affected version
Tested affected: 8.17.0, 8.21.0, 8.22.0, 8.23.0, master 59d2537b40, Homebrew 8.22.0 and the macOS system /usr/bin/curl 8.7.1. The imap://u%40d@host/ form (any user name containing @, \ or /) was accepted by every version I tested. The empty-userinfo form was accepted by all of them except 8.20.0, where 425a2aa1af's data->conn->user[0] check happened to block it, and also broke the user's own ambient-credential transfers, until the 8.21.0 credentials rework 8f71d0fde5 made a creds object non-NULL for an empty user again; by those commits the gap should start at 8.18.0, but 8.20.0 is the only release in it I built. SASL GSSAPI exists since 7.40.0 (018b9d421a) and the libssh GSSAPI path since 7.58.0 (c92d2e14cf); I did not build those. The mail protocols affect libcurl and the curl tool. For SSH it is libcurl applications that leave CURLOPT_SSH_KNOWNHOSTS unset, which is the libcurl default, or that accept new host keys; the curl tool is not exposed since 8.23.0, because the CVE-2026-86410 fix 6ae9befb2f sets the SSH options on redirects as well and falls back to $HOME/.ssh/known_hosts even with no --knownhosts (src/config2setopts.c:238-254), so the attacker's host key is refused before authentication. macOS 27.0.1 arm64, MIT krb5 1.22.2, libssh 0.12.0.
Steps To Reproduce:
Everything runs on 127.0.0.1. The scripts build a throwaway MIT realm in ./krb with their own KRB5_CONFIG, KRB5CCNAME and KRB5_KDC_PROFILE, so the host's Kerberos setup, the real ~/.ssh and the real ccache are untouched; host names are mapped with --resolve.
./setup_kdc.shcreates realm CREDS.TEST, the victimalice, the attacker's service keysimap|pop|smtp|HTTP|host/b.testinkrb/b.keytab, and buildsgssaccept, which validates a captured token against that keytab.CURL=/path/to/curl ./run_sasl_gssapi_poc.shstarts the KDC, runskinit alice, startssrv.py(answers302 Location: imap://@b.test:21105/) andimapsrv.py(offers only AUTH=GSSAPI and logs the token), then runs with no authentication options at all:curl -L --proto-redir =http,https,imap --resolve a.test:21101:127.0.0.1 --resolve b.test:21105:127.0.0.1 'http://a.test:21101/x?code=302&loc=imap://@b.test:21105/'and prints whatgssacceptmakes of the token. It then repeats this for pop3 and smtp, for theimap://u%[email protected]/form, with--delegation always, and for the reference, mitigation and opt-in cases../build.sh [curl build dir] && ./run_libcurl_poc.shdoes the same from a libcurl application that sets only CURLOPT_FOLLOWLOCATION and CURLOPT_REDIR_PROTOCOLS_STR"http,https,imap".cd libssh && ./build.sh <curl build dir with -DCURL_USE_LIBSSH=ON> && ./run_libssh_poc.shbuilds the application,evilsshdand its host key, and follows302 Location: sftp://b.test:23220/some/file.- The realm lives entirely in
./krb; delete that directory to clean up.
"ACCEPTED" means gssaccept validated the captured token against the attacker's keytab as [email protected]. Evidence 01 and 02 for SASL, 03 for SSH, 04 for the release range.
| version | imap @ | pop3 @ | smtp @ | imap u%40d@ | @ + --delegation | own direct transfer |
|---|---|---|---|---|---|---|
| 8.17.0 | ACCEPTED | ACCEPTED | ACCEPTED | ACCEPTED | ACCEPTED + TGT | ACCEPTED |
| 8.20.0 | no token | no token | no token | ACCEPTED | no token | no token |
| 8.21.0 / 8.22.0 / 8.23.0 / master | ACCEPTED | ACCEPTED | ACCEPTED | ACCEPTED | ACCEPTED + TGT | ACCEPTED |
| Homebrew 8.22.0, macOS 8.7.1 | ACCEPTED | ACCEPTED | ACCEPTED | ACCEPTED | ACCEPTED + TGT | ACCEPTED |
| master + attached fix | no token | no token | no token | no token | no token | ACCEPTED |
| libssh backend, 8.23.0 and master | result |
|---|---|
302 to sftp:// and to scp:// | ACCEPTED (965 b), local login name sent as the SSH user |
same, GSSAPIDelegateCredentials yes | ACCEPTED (1733 b) + DELEGATED TGT |
| master + attached fix | no gssapi attempt; own direct sftp:// still ACCEPTED |
| only http,https in redirects | CURLE_UNSUPPORTED_PROTOCOL |
| no Kerberos ticket | no gssapi attempt |
| the curl tool, same redirect | exit 60, host key refused first, 0 tokens (CVE-2026-86410 fix) |
HTTP control, same script: 8.22.0 sends Authorization: Negotiate to the redirect target, 8.23.0 and master send none. Mitigation --disallow-username-in-url: no token, both Location forms, every affected build. Opt-in --location-trusted: authenticates, before and after the fix.
Fix
fix/sasl-libssh-gssapi-allowed-origin.diff (against master 59d2537b40) requires Curl_auth_allowed_to_host() before the ambient Kerberos identity is used, in sasl_choose_krb5() for the mail protocols and in myssh_in_AUTH_GSSAPI() for libssh, which is the rule 557a52f21e applied to HTTP. Built and re-run on all three builds (evidence 01, 02, 03): no token reaches any redirect target, the user's own direct transfers still authenticate, and --location-trusted still works. One side effect: for the mail protocols SASL then falls back to a plain LOGIN carrying the Location's own (empty or attacker-chosen) user name and no password, so nothing secret is sent but the exchange is no longer identical.
The two halves are independent: the SASL finding needs no libssh, and the diff patches the two call sites separately. If you would rather track the SSH half as its own report, say so and I will file it separately rather than have it hold up the mail protocols.
AI disclosure: I used an AI assistant (Claude) for code review, test setup and drafting. Every row above was produced by the attached scripts on my own machine and the zip holds the raw logs. Before filing I re-ran the SASL matrix myself on two binaries, a curl-8_23_0 build and the macOS system /usr/bin/curl 8.7.1 as shipped, covering the HTTP control, imap/pop3/smtp, the u%40d@ form, --delegation always, both --disallow-username-in-url cases and --location-trusted; both matched their rows above.
Impact
A malicious HTTP server, or anyone who can answer one plaintext HTTP request, makes curl mint a Kerberos AP-REQ for a service and host the attacker picks (imap|pop|smtp|host/<their host>) out of the user's ambient identity, with no credentials, no Kerberos options and no authentication method set by the user. If the attacker owns that service principal, which any Active Directory domain user can arrange by default since machine accounts may be created with SPNs, they learn the user's identity and the session key; SASL GSSAPI has no channel binding here, so an attacker who can also intercept the connection to a real mail server can relay the token to it. With --delegation always, or GSSAPIDelegateCredentials yes for libssh, the forwarded ticket is the user's TGT, which is full impersonation in the realm for its lifetime. The conditions match CVE-2026-86413 and CVE-2026-94265 (both Low) with the mail protocols in place of HTTP, except that those needed a first transfer "doing Negotiate authentication" and this needs no authentication option at all. Redirects have to allow imap, pop3, smtp, sftp or scp, which is not the default: the same precondition CVE-2025-14524 called "A highly unusual combination" before it was given a CVE at Low. --disallow-username-in-url blocks the mail half, and Fedora and RHEL ship the libssh backend.