CVE-2026-9547: SSH improper host validation
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
The available evidence does not settle the report. A maintainer should investigate.
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.
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
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.
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.
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.
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.
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)
Source code
| 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; |