✓ProveIt
Examples / Report #3797526 Saved analysis

CVE-2026-12064: proto-default skips SSH verification

curl · 2026-06-1215.1s analysis · $0.015 in model calls

What the report claims

curl silently skips SSH host key verification when using --proto-default sftp with a schemeless URL, allowing MITM credential theft.

Compare with curl's recorded outcome

Known vulnerability

A real vulnerability: CVE-2026-12064 proto-default skips SSH verification

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?
  • The tool layer (config2setopts.c:190) skips SSH host key options when use_proto is guessed as HTTP for schemeless URLs
  • Libcurl (url.c:1664-1668) later applies --proto-default and rewrites the URL to sftp://, so the actual transfer uses SFTP
  • The user's explicit --hostpubsha256 is silently ignored, allowing credential disclosure to a MITM attacker
  • The PoC confirms the vulnerability on current master (8.21.0-DEV)
How this analysis ran 5 claims · 10 citations verified

Checked against curl master on 2026-06-12 (d77b4b740734). Dev 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 · 16 references checked out, 0 did not
2 · Claims
NVIDIA-Nemotron-3-Nano-30B-A3B
5 claims · 3426 tokens · $0.0003
3 · Evidence
3 code snippets
real curl source at d77b4b740734 · 3 docs via Tavily
4 · Judgment
Nemotron-3-Ultra-550b-a55b
10819 tokens · 11.9s · $0.0148
5 · Receipts
10 of 10 citations verified
every citation must point at code the judge was shown
Review a draft reply to the reporter

Edit and verify before sending.

This is a valid security vulnerability. The tool layer guesses HTTP for schemeless URLs (config2setopts.c:146,190), skipping SSH host key options, but libcurl later applies --proto-default and connects via SFTP (url.c:1664-1668). The user's --hostpubsha256 is silently ignored. The PoC demonstrates credential capture by a rogue SSH server. We'll fix the tool layer to respect --proto-default when deciding whether to set SSH options.

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

Report claims

C1flaw supported

curl skips setting CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256, CURLOPT_SSH_KNOWNHOSTS, and other SSH security options when it guesses the URL scheme as HTTP for a schemeless host

Read the reasoning

The tool layer's url_proto_and_rewrite uses CURLU_GUESS_SCHEME for schemeless URLs, guessing HTTP. This makes use_proto != proto_scp/sftp, so ssh_setopts returns early at line 190 without setting CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256 or CURLOPT_SSH_KNOWNHOSTS. But libcurl's parseurlandfillconn later applies --proto-default and rewrites the URL to sftp://, so the actual transfer uses SFTP without the configured host key verification.

C2location supported

src/config2setopts.c:190

Read the reasoning

Line 190 in src/config2setopts.c is the gate that skips SSH options when use_proto is not SCP or SFTP.

C3trigger supported

user invokes curl with a schemeless URL and --proto-default sftp (or scp)

Read the reasoning

The trigger is exactly as described: a schemeless URL with --proto-default sftp causes the tool layer to guess HTTP while libcurl later uses SFTP.

C4impact supported

connection proceeds without host key verification, sending credentials to an unverified SSH server

Read the reasoning

The SSH host key verification options are never set (lines 203-215 skipped), but libcurl connects via SFTP anyway (line 1666), so credentials are sent to an unverified host.

C5reproduction supported

run curl with --proto-default sftp and a schemeless URL against a rogue SSH server that captures credentials

Read the reasoning

The PoC demonstrates the exact attack: schemeless URL with --proto-default sftp and --hostpubsha256 causes credentials to be sent to a rogue SSH server because the pin check was skipped.

Read the original report (excerpt)

Full report on HackerOne ↗

### Summary When a user invokes curl with a schemeless URL and `--proto-default sftp` (or `scp`), the tool layer guesses the URL is HTTP and skips setting SSH security options (`CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256`, `CURLOPT_SSH_KNOWNHOSTS`). However libcurl's runtime correctly applies `--proto-default` and connects via SFTP/SCP. The result is that `--hostpubsha256`, `--hostpubmd5`, and `--knownhosts` are silently ignored — curl connects to an unverified SSH host and sends authentication credentials. A network MITM attacker running a rogue SSH server captures the victim's password or triggers key-based auth against an attacker-controlled host, while curl reports no host key error. ### Affected version Introduced in commit `f97d372703` (2025-05-15, "tool_operate: move config2setopts to separate file"), first released in **curl 8.14.0**. Present through current master (8.21.0-DEV). ### Root cause In `src/config2setopts.c`, `url_proto_and_rewrite()` uses `CURLU_GUESS_SCHEME` to determine `use_proto`. For a schemeless URL like `host:2222/path`, it guesses HTTP. The SSH options gate on this guess: ```c // src/config2setopts.c:190 if(use_proto != proto_scp && use_proto != proto_sftp) return CURLE_OK; // skips setting CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256, CURLOPT_SSH_KNOWNHOSTS, etc. ``` But `CURLOPT_DEFAULT_PROTOCOL` is set later (line ~1053), and libcurl's `lib/url.c:1662` prepends `sftp://` at runtime. So the actual transfer uses SFTP with no host key verification configured. ### Steps To Reproduce The PoC starts a Paramiko SSH server, then runs curl twice: once with explicit `sftp://` (host key check works) and once with `--proto-default sftp` + schemeless URL (host key check bypassed, password captured). Prerequisites: `pip install paramiko`. Run from the curl source tree root (`~/curl/curl`). ```bash #!/usr/bin/env bash # Run from the curl source root: ~/curl/curl # The local build at ./build-ssh/src/curl must have SFTP support (libssh2). set -e CURL_BIN="./build-ssh/src/curl" export LD_LIBRARY_PATH="/tmp/libssh2-install/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" echo "[*] Using: $CURL_BIN" "$CURL_BIN" --version | head -2 if ! "$CURL_BIN" --version | grep -q sftp; then echo "FATAL: $CURL_BIN does not have SFTP support."; exit 1 fi echo "" python3 - "$CURL_BIN" <<'PYEOF' import os, sys, socket, threading, time, subprocess CURL_BIN = sys.argv[1 …

Source code

src/config2setopts.clines 134–215 · lines 190-190 cited by the report; definition of url_proto_and_rewrite
134static CURLcode url_proto_and_rewrite(char **url,
135 struct OperationConfig *config,
136 const char **scheme)
137{
138 CURLcode result = CURLE_OK;
139 CURLU *uh = curl_url();
140 const char *proto = NULL;
141 *scheme = NULL;
142
143 DEBUGASSERT(url && *url);
144 if(uh) {
145 char *schemep = NULL;
146 CURLUcode uc =
C1: url_proto_and_rewrite uses CURLU_GUESS_SCHEME when parsing schemeless URLs, causing HTTP guess
C3: CURLU_GUESS_SCHEME used when parsing schemeless URLs
147 curl_url_set(uh, CURLUPART_URL, *url,
148 CURLU_GUESS_SCHEME | CURLU_NON_SUPPORT_SCHEME);
149 if(!uc) {
150 uc = curl_url_get(uh, CURLUPART_SCHEME, &schemep, CURLU_DEFAULT_SCHEME);
151 if(!uc) {
152#ifdef CURL_DISABLE_IPFS
153 (void)config;
154#else
155 if(curl_strequal(schemep, proto_ipfs) ||
156 curl_strequal(schemep, proto_ipns)) {
157 result = ipfs_url_rewrite(uh, schemep, url, config);
158 /* short-circuit proto_token, we know it is ipfs or ipns */
159 if(curl_strequal(schemep, proto_ipfs))
160 proto = proto_ipfs;
161 else if(curl_strequal(schemep, proto_ipns))
162 proto = proto_ipns;
163 if(result)
164 config->synthetic_error = TRUE;
165 }
166 else
167#endif /* !CURL_DISABLE_IPFS */
168 proto = proto_token(schemep);
169 curl_free(schemep);
170 }
171 else if(uc == CURLUE_OUT_OF_MEMORY)
172 result = CURLE_OUT_OF_MEMORY;
173 }
174 else if(uc == CURLUE_OUT_OF_MEMORY)
175 result = CURLE_OUT_OF_MEMORY;
176 curl_url_cleanup(uh);
177 }
178 else
179 result = CURLE_OUT_OF_MEMORY;
180
181 *scheme = proto ? proto : "?"; /* Never match if not found. */
182 return result;
183}
184
185static CURLcode ssh_setopts(struct OperationConfig *config, CURL *curl,
186 const char *use_proto)
187{
188 CURLcode result;
189
190 if(use_proto != proto_scp && use_proto != proto_sftp)
C1: ssh_setopts returns early if use_proto is not proto_scp or proto_sftp, skipping SSH host key options
C2: The exact line cited: if(use_proto != proto_scp && use_proto != proto_sftp) return CURLE_OK;
C4: Early return skips setting CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256 (line 203) and CURLOPT_SSH_KNOWNHOSTS (line 211+)
C5: The code path that skips SSH options is reachable via schemeless URL + --proto-default
191 return CURLE_OK;
192
193 /* SSH and SSL private key uses same command-line option */
194 MY_SETOPT_STR(curl, CURLOPT_SSH_PRIVATE_KEYFILE, config->key);
195 MY_SETOPT_STR(curl, CURLOPT_SSH_PUBLIC_KEYFILE, config->pubkey);
196
197 /* SSH host key md5 checking allows us to fail if we are not talking to who
198 we think we should */
199 MY_SETOPT_STR(curl, CURLOPT_SSH_HOST_PUBLIC_KEY_MD5, config->hostpubmd5);
200
201 /* SSH host key sha256 checking allows us to fail if we are not talking to
202 who we think we should */
203 MY_SETOPT_STR(curl, CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256,
204 config->hostpubsha256);
205
206 if(config->ssh_compression)
207 my_setopt_long(curl, CURLOPT_SSH_COMPRESSION, 1);
208
209 if(!config->insecure_ok) {
210 char *known = config->knownhosts;
211 if(!known) {
212 char *found = findfile(".ssh/known_hosts", FALSE);
213 if(found) {
214 known = curlx_strdup(found);
215 curl_free(found);
lib/url.clines 1637–1687 · lines 1662-1662 cited by the report
1637/*
1638 * Parse URL and fill in the relevant members of the connection struct.
1639 */
1640static CURLcode parseurlandfillconn(struct Curl_easy *data,
1641 struct connectdata *conn)
1642{
1643 CURLcode result = CURLE_OK;
1644 CURLU *uh;
1645 CURLUcode uc;
1646 bool use_set_uh = (data->set.uh && !data->state.this_is_a_follow);
1647 uint16_t port_override = data->state.allow_port ? data->set.use_port : 0;
1648 uint32_t scope_id = 0;
1649
1650 up_free(data); /* cleanup previous leftovers first */
1651
1652 /* parse the URL */
1653 if(use_set_uh)
1654 uh = data->state.uh = curl_url_dup(data->set.uh);
1655 else
1656 uh = data->state.uh = curl_url();
1657 if(!uh) {
1658 result = CURLE_OUT_OF_MEMORY;
1659 goto out;
1660 }
1661
1662 /* Calculate the *real* URL this transfer uses, applying defaults
1663 * where information is missing. */
1664 if(data->set.str[STRING_DEFAULT_PROTOCOL] &&
C1: libcurl later applies CURLOPT_DEFAULT_PROTOCOL (from --proto-default) and prepends sftp:// to schemeless URLs
C3: CURLOPT_DEFAULT_PROTOCOL (set by --proto-default) applied later in libcurl
C5: The code path that applies --proto-default and uses SFTP is reachable
1665 !Curl_is_absolute_url(Curl_bufref_ptr(&data->state.url), NULL, 0, TRUE)) {
1666 char *url = curl_maprintf("%s://%s",
C4: URL rewritten to sftp://... so actual connection uses SFTP
1667 data->set.str[STRING_DEFAULT_PROTOCOL],
1668 Curl_bufref_ptr(&data->state.url));
1669 if(!url) {
1670 result = CURLE_OUT_OF_MEMORY;
1671 goto out;
1672 }
1673 Curl_bufref_set(&data->state.url, url, 0, curl_free);
1674 }
1675
1676 if(!use_set_uh) {
1677 char *newurl;
1678 uc = curl_url_set(uh, CURLUPART_URL, Curl_bufref_ptr(&data->state.url),
1679 (unsigned int)(CURLU_GUESS_SCHEME |
1680 CURLU_NON_SUPPORT_SCHEME |
1681 (data->set.disallow_username_in_url ?
1682 CURLU_DISALLOW_USER : 0) |
1683 (data->set.path_as_is ? CURLU_PATH_AS_IS : 0)));
1684 if(uc) {
1685 failf(data, "URL rejected: %s", curl_url_strerror(uc));
1686 result = Curl_uc_to_curlcode(uc);
1687 goto out;
include/curl/urlapi.hlines 96–96 · definition of CURLU_GUESS_SCHEME
Show 1 lines (not cited by the judge)
96#define CURLU_GUESS_SCHEME (1 << 9) /* legacy curl-style guessing */