✓ProveIt
Examples / Report #3751712 Saved analysis

CVE-2026-9547: SSH improper host validation

curl · 2026-05-2014.1s analysis · $0.017 in model calls

What the report claims

curl incorrectly maps libssh's SSH_KNOWN_HOSTS_OTHER state to CURLKHMATCH_MISSING, causing applications using CURLOPT_SSH_KEYFUNCTION to accept host keys of different types than those recorded in known_hosts, which libssh classifies as a possible attack.

Compare with curl's recorded outcome

Known vulnerability

A real vulnerability: CVE-2026-9547 SSH improper host validation

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

ProveIt's finding

Needs human review

The available evidence does not settle the report. A maintainer should investigate.

Judgment withdrawn. The answer failed the evidence check.
See the withdrawn model reasoning

The following reasoning failed the citation check and does not support the final finding.

  • curl's libssh backend maps SSH_KNOWN_HOSTS_OTHER to CURLKHMATCH_MISSING (lines 279-283) instead of CURLKHMATCH_MISMATCH (line 286)
  • libssh explicitly documents SSH_KNOWN_HOSTS_OTHER as a security-relevant state: 'An attacker might change the default server key to confuse your client into thinking the key does not exist'
  • Applications using CURLOPT_SSH_KEYFUNCTION with TOFU pattern (MISSING -> FINE_ADD_TO_FILE, MISMATCH -> REJECT) will silently accept a server presenting a different key type when only a different key type is recorded in known_hosts
  • The flaw is reachable by a remote attacker controlling the SSH server, not requiring local access
  • The suggested fix (mapping OTHER to MISMATCH) aligns with libssh's own security classification
How this analysis ran 5 claims · 11 citations verified

Evidence check: judge quoted an external source that does not say that (https://api.libssh.org/master/libssh_tutor_guided_tour.html)

Checked against curl master on 2026-05-20 (d3b04e560036). 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
partially grounded
no model · 14 references checked out, 2 did not
2 · Claims
NVIDIA-Nemotron-3-Nano-30B-A3B
5 claims · 3403 tokens · $0.0003
3 · Evidence
1 code snippets
real curl source at d3b04e560036 · 3 docs via Tavily
4 · Judgment
Nemotron-3-Ultra-550b-a55b
11229 tokens · 10.4s · $0.0166
5 · Receipts
11 of 13 citations verified
every citation must point at code the judge was shown
Inspect the withdrawn draft reply

This draft belongs to the withdrawn judgment. It may contain unsupported claims.

Thank you for the detailed report. The code confirms that lib/vssh/libssh.c maps SSH_KNOWN_HOSTS_OTHER to CURLKHMATCH_MISSING (lines 279-283) while libssh documents this state as a possible attack. This causes TOFU callbacks to silently accept a key type mismatch. The fix to map it to CURLKHMATCH_MISMATCH is correct. We'll address this.

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

This model judgment was withdrawn. The claim assessments below are preserved for inspection and may be wrong.

Report claims

C1flaw supported

curl maps SSH_KNOWN_HOSTS_OTHER to CURLKHMATCH_MISSING instead of CURLKHMATCH_MISMATCH

Read the reasoning

The switch statement at lines 275-288 explicitly groups SSH_KNOWN_HOSTS_OTHER with the MISSING cases (lines 279-283), while the default case (which catches SSH_KNOWN_HOSTS_CHANGED) sets MISMATCH (line 286). This confirms the claim.

C2location supported

curl's switch in lib/vssh/libssh.c at lines 275-288 groups SSH_KNOWN_HOSTS_OTHER with unknown/first contact cases

Read the reasoning

The code at lines 275-288 shows SSH_KNOWN_HOSTS_OTHER grouped with NOT_FOUND, UNKNOWN, and ERROR cases, all mapping to CURLKHMATCH_MISSING. The report's description of the grouping is accurate.

C3trigger supported

libssh returns SSH_KNOWN_HOSTS_OTHER when a known host exists but the server key type differs from recorded key types

Read the reasoning

The external source confirms libssh treats SSH_KNOWN_HOSTS_OTHER as a security-relevant state indicating a possible attack where an attacker presents a different key type. The report's description of when libssh returns this state (host known but only under different key types) matches the libssh documentation.

C4impact supported

Applications using CURLOPT_SSH_KEYFUNCTION with TOFU pattern will silently accept mismatched host key types

Read the reasoning

When SSH_KNOWN_HOSTS_OTHER is mapped to CURLKHMATCH_MISSING, a TOFU-pattern callback (which treats MISSING as FINE_ADD_TO_FILE and MISMATCH as REJECT) will silently accept the connection. The code path exists: libssh returns OTHER -> curl maps to MISSING -> callback returns FINE_ADD_TO_FILE -> connection proceeds.

C5reproduction supported

Reproducer uses libssh-only server with Ed25519 key and known_hosts containing only RSA entry to trigger SSH_KNOWN_HOSTS_OTHER

Read the reasoning

The reproducer setup (Ed25519 server key, known_hosts with only RSA entry) matches the condition libssh documents for SSH_KNOWN_HOSTS_OTHER. The code path is reachable and the PoC demonstrates the TOFU callback behavior.

Deterministic checks that failed

  • quote 2 of 9 quoted lines appear nowhere in curl, e.g. 'case SSH_KNOWN_HOSTS_OTHER: /* downgraded to MISSING */'
  • quote only 1/9 quoted lines appear anywhere in curl at d3b04e560036

What the report says about other projects

confirmed The server gave us a key of a type while we had an other type recorded. It is a possible attack.

“The host key for this server was not found but an other type of key exists. An attacker might change the default server key to confuse your client into thinking the key does not exist” source

Read the original report (excerpt)

Full report on HackerOne ↗

Hi all, We've found an issue in `lib/vssh/libssh.c` where the libssh backend maps `SSH_KNOWN_HOSTS_OTHER` to `CURLKHMATCH_MISSING` instead of `CURLKHMATCH_MISMATCH`. libssh documents `SSH_KNOWN_HOSTS_OTHER` as *"The server gave us a key of a type while we had another type recorded. It is a possible attack."* (`/usr/include/libssh/libssh.h:254`) curl's switch at `lib/vssh/libssh.c:275-288` groups it with the "unknown / first contact" cases: ```c case SSH_KNOWN_HOSTS_OK: keymatch = CURLKHMATCH_OK; break; case SSH_KNOWN_HOSTS_OTHER: /* downgraded to MISSING */ case SSH_KNOWN_HOSTS_NOT_FOUND: case SSH_KNOWN_HOSTS_UNKNOWN: case SSH_KNOWN_HOSTS_ERROR: keymatch = CURLKHMATCH_MISSING; break; default: /* SSH_KNOWN_HOSTS_CHANGED */ keymatch = CURLKHMATCH_MISMATCH; break; ```` Applications using a `CURLOPT_SSH_KEYFUNCTION` that follows a TOFU pattern (`MISSING` -> `CURLKHSTAT_FINE_ADD_TO_FILE`; `MISMATCH` -> `CURLKHSTAT_REJECT`) will silently accept a server that presents a host key whose base key type has no matching entry for that host, even though another key type is already recorded in known_hosts. libssh classifies that condition as `SSH_KNOWN_HOSTS_OTHER` and describes it as a possible attack, but curl reports it to the application as an "unknown new host" event. The libssh2 backend has no equivalent state and is unaffected. This dates to commit `507cf6a13d` (2020-02-21, "libssh: improve known hosts handling"), which migrated host-key verification to the newer `ssh_session_is_known_server()` API and grouped `SSH_KNOWN_HOSTS_OTHER` with the "unknown / first contact" cases in the switch, instead of letting it fall through to the default `CURLKHMATCH_MISMATCH` arm. ## How `SSH_KNOWN_HOSTS_OTHER` is reached in libssh In libssh, `SSH_KNOWN_HOSTS_OTHER` is returned when the host is present in known_hosts, but the server key negotiated for the current session has a different base key type from the recorded key or keys, and no same-type mismatch was found. The relevant libssh logic is in `ssh_known_hosts_check_server_key()` in `src/knownhosts.c`: Reference: `https://git.libssh.org/projects/libssh.git/tree/src/knownhosts.c` The matching logic is effectively: ```c found = SSH_KNOWN_HOSTS_UNKNOWN; for each known_hosts entry matching this host: if(server_key == entry_key) return SSH_KNOWN_HOSTS_OK; i …

Source code

lib/vssh/libssh.clines 250–313 · lines 275-288 cited by the report
250 knownkey.keytype = CURLKHTYPE_RSA;
251 break;
252 case SSH_KEYTYPE_RSA1:
253 knownkey.keytype = CURLKHTYPE_RSA1;
254 break;
255 case SSH_KEYTYPE_ECDSA:
256 case SSH_KEYTYPE_ECDSA_P256:
257 case SSH_KEYTYPE_ECDSA_P384:
258 case SSH_KEYTYPE_ECDSA_P521:
259 knownkey.keytype = CURLKHTYPE_ECDSA;
260 break;
261 case SSH_KEYTYPE_ED25519:
262 knownkey.keytype = CURLKHTYPE_ED25519;
263 break;
264 case SSH_KEYTYPE_DSS:
265 knownkey.keytype = CURLKHTYPE_DSS;
266 break;
267 default:
268 rc = SSH_ERROR;
269 goto cleanup;
270 }
271 knownkeyp = &knownkey;
272 }
273 }
274
275 switch(vstate) {
C2: switch(vstate) begins at line 275
276 case SSH_KNOWN_HOSTS_OK:
277 keymatch = CURLKHMATCH_OK;
278 break;
279 case SSH_KNOWN_HOSTS_OTHER:
C1: case SSH_KNOWN_HOSTS_OTHER: falls through to line 283 where keymatch = CURLKHMATCH_MISSING
C2: case SSH_KNOWN_HOSTS_OTHER: at line 279
C5: SSH_KNOWN_HOSTS_OTHER case exists and is reachable when libssh returns this state
280 case SSH_KNOWN_HOSTS_NOT_FOUND:
C2: case SSH_KNOWN_HOSTS_NOT_FOUND: at line 280
281 case SSH_KNOWN_HOSTS_UNKNOWN:
C2: case SSH_KNOWN_HOSTS_UNKNOWN: at line 281
282 case SSH_KNOWN_HOSTS_ERROR:
C2: case SSH_KNOWN_HOSTS_ERROR: at line 282
283 keymatch = CURLKHMATCH_MISSING;
C1: keymatch = CURLKHMATCH_MISSING for SSH_KNOWN_HOSTS_OTHER, SSH_KNOWN_HOSTS_NOT_FOUND, SSH_KNOWN_HOSTS_UNKNOWN, SSH_KNOWN_HOSTS_ERROR
C4: keymatch = CURLKHMATCH_MISSING for SSH_KNOWN_HOSTS_OTHER
284 break;
285 default:
286 keymatch = CURLKHMATCH_MISMATCH;
C1: default case (SSH_KNOWN_HOSTS_CHANGED) sets keymatch = CURLKHMATCH_MISMATCH
287 break;
288 }
289
290 if(func) { /* use callback to determine action */
C4: if(func) { /* use callback to determine action */ - shows CURLOPT_SSH_KEYFUNCTION callback is invoked
291 rc = ssh_pki_export_pubkey_base64(pubkey, &found_base64);
292 if(rc != SSH_OK)
293 goto cleanup;
294
295 foundkey.key = found_base64;
296 foundkey.len = strlen(found_base64);
297
298 switch(ssh_key_type(pubkey)) {
299 case SSH_KEYTYPE_RSA:
300 foundkey.keytype = CURLKHTYPE_RSA;
301 break;
302 case SSH_KEYTYPE_RSA1:
303 foundkey.keytype = CURLKHTYPE_RSA1;
304 break;
305 case SSH_KEYTYPE_ECDSA:
306 case SSH_KEYTYPE_ECDSA_P256:
307 case SSH_KEYTYPE_ECDSA_P384:
308 case SSH_KEYTYPE_ECDSA_P521:
309 foundkey.keytype = CURLKHTYPE_ECDSA;
310 break;
311 case SSH_KEYTYPE_ED25519:
312 foundkey.keytype = CURLKHTYPE_ED25519;
313 break;