Missing server identity policy enforcement in SSH connection reuse allows host key verification bypass via pool poisoning
High
Vulnerability Details
# Missing server identity policy enforcement in SSH connection reuse allows host key verification bypass via pool poisoning
---
## Summary
`ssh_config_matches()` in `lib/url.c` decides whether an existing SSH connection can be reused by a new transfer handle. It checks client key paths (`rsa`, `rsa_pub`) but never inspects the three options that control server identity verification: `STRING_SSH_KNOWNHOSTS`, `STRING_SSH_HOST_PUBLIC_KEY_MD5`, and `STRING_SSH_HOST_PUBLIC_KEY_SHA256`. A handle configured with strict host key pinning reuses a pooled connection that was established with zero server verification — the pinning check never fires because the SSH handshake is skipped entirely.
This is the same bug class as CVE-2022-27782 and CVE-2023-27538. Both fixed client-side key options. The server-side identity options were never added.
---
## Vulnerability Details
- **Vulnerability Type:** Authentication Bypass (Connection Pool Poisoning)
- **CVSS 3.1 Score:** 7.5 (High) — `AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:N/A:N`
- **Affected Component:** `lib/url.c` → `ssh_config_matches()`
- **Affected Protocols:** SFTP, SCP
---
## Root Cause
The full function body at `lib/url.c`:
```c
static bool ssh_config_matches(struct connectdata *one,
struct connectdata *two)
{
struct ssh_conn *sshc1, *sshc2;
sshc1 = Curl_conn_meta_get(one, CURL_META_SSH_CONN);
sshc2 = Curl_conn_meta_get(two, CURL_META_SSH_CONN);
return sshc1 && sshc2 && Curl_safecmp(sshc1->rsa, sshc2->rsa) &&
Curl_safecmp(sshc1->rsa_pub, sshc2->rsa_pub);
}
```
Three `data->set.str[]` entries are absent from this comparison:
| Option | String Key | Purpose |
|--------|------------|---------|
| `CURLOPT_SSH_KNOWNHOSTS` | `STRING_SSH_KNOWNHOSTS` | Path to known_hosts file for server fingerprint validation |
| `CURLOPT_SSH_HOST_PUBLIC_KEY_MD5` | `STRING_SSH_HOST_PUBLIC_KEY_MD5` | Expected MD5 hash of server's public key |
| `CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256` | `STRING_SSH_HOST_PUBLIC_KEY_SHA256` | Expected SHA-256 hash of server's public key |
For contrast, the TLS backend enforces full parity on security-relevant fields during connection reuse — `verifypeer`, `verifyhost`, pinned certificates are all compared. The SSH backend has no equivalent enforcement.
---
## Steps to Reproduce
**Environment:**
- libcurl built with libssh2 (default SSH backend)
- Two easy handles sharing one connection pool via `CURLSH`
- Target: any SFTP server (e.g. `test.rebex.net`, public demo)
```
./poc
```
**Source (`poc.c`):**
```c
#include <curl/curl.h>
#include <stdio.h>
int main(void) {
CURL *relaxed, *strict;
CURLSH *pool;
long new_connections = 0;
curl_global_init(CURL_GLOBAL_DEFAULT);
pool = curl_share_init();
curl_share_setopt(pool, CURLSHOPT_SHARE, CURL_LOCK_DATA_CONNECT);
relaxed = curl_easy_init();
strict = curl_easy_init();
if(relaxed && strict && pool) {
/* --- Transfer 1: no host verification --- */
curl_easy_setopt(relaxed, CURLOPT_URL, "sftp://demo:[email protected]/readme.txt");
curl_easy_setopt(relaxed, CURLOPT_SHARE, pool);
// Silencing stderr for clean output
curl_easy_setopt(relaxed, CURLOPT_NOBODY, 1L);
fprintf(stderr, "[*] Transfer 1 — no host key checks\n");
curl_easy_perform(relaxed);
/* --- Transfer 2: strict host key pinning requested --- */
curl_easy_setopt(strict, CURLOPT_URL, "sftp://demo:[email protected]/readme.txt");
curl_easy_setopt(strict, CURLOPT_SHARE, pool);
curl_easy_setopt(strict, CURLOPT_SSH_KNOWNHOSTS, "/non/existent/file");
curl_easy_setopt(strict, CURLOPT_NOBODY, 1L);
fprintf(stderr, "[*] Transfer 2 — CURLOPT_SSH_KNOWNHOSTS set to non-existent file\n");
curl_easy_perform(strict);
curl_easy_getinfo(strict, CURLINFO_NUM_CONNECTS, &new_connections);
if(new_connections == 0) {
fprintf(stderr, "\n[!] VULNERABLE — Transfer 2 reused the unverified connection.\n"
" CURLOPT_SSH_KNOWNHOSTS was silently ignored.\n"
" No new TCP or SSH handshake occurred.\n");
} else {
fprintf(stderr, "\n[+] NOT VULNERABLE — new connection was established.\n");
}
}
curl_easy_cleanup(relaxed);
curl_easy_cleanup(strict);
curl_share_cleanup(pool);
curl_global_cleanup();
return 0;
}
```
**Output on vulnerable build:**
```
[*] Transfer 1 — no host key checks
[*] Transfer 2 — CURLOPT_SSH_KNOWNHOSTS set to non-existent file
[!] VULNERABLE — Transfer 2 reused the unverified connection.
CURLOPT_SSH_KNOWNHOSTS was silently ignored.
No new TCP or SSH handshake occurred.
```
{F5648299}
**Expected behavior:** Transfer 2 opens a new connection, attempts to read `/tmp/does_not_exist_known_hosts`, fails, and returns `CURLE_SSH` error.
**Actual behavior:** Transfer 2 reuses the pooled connection from Transfer 1. `CURLINFO_NUM_CONNECTS` returns `0` — no handshake, no host key check, no error.
---
## Impact
In any environment where multiple transfer handles share a connection pool — `CURLM` multi-handle applications, `CURLSH` shared caches, PHP-FPM worker pools, proxy daemons — a connection established without server identity verification is reusable by handles that explicitly require it.
An attacker who performs a MITM on the initial unverified connection inherits access to every subsequent transfer on that pool, including those configured with host key pinning or `known_hosts` validation. Sensitive SFTP/SCP data (credentials, files, configuration) flows over the attacker-controlled channel. The developer's explicit security policy is discarded without error or warning.
This is not theoretical — `CURLSH` shared pools exist precisely for multi-tenant connection reuse. The TLS backend already prevents this exact pattern. The SSH backend does not.
**Prior art:**
- **CVE-2022-27782** — fixed TLS + partial SSH reuse checks ($2,400)
- **CVE-2023-27538** — fixed remaining client key options ($480)
- **This report** — server identity options remain unchecked
---
## Recommended Fix
Add three comparisons to `ssh_config_matches()`:
```c
static bool ssh_config_matches(struct connectdata *one,
struct connectdata *two)
{
struct ssh_conn *sshc1, *sshc2;
sshc1 = Curl_conn_meta_get(one, CURL_META_SSH_CONN);
sshc2 = Curl_conn_meta_get(two, CURL_META_SSH_CONN);
return sshc1 && sshc2 &&
Curl_safecmp(sshc1->rsa, sshc2->rsa) &&
Curl_safecmp(sshc1->rsa_pub, sshc2->rsa_pub) &&
Curl_safecmp(one->data->set.str[STRING_SSH_KNOWNHOSTS],
two->data->set.str[STRING_SSH_KNOWNHOSTS]) &&
Curl_safecmp(one->data->set.str[STRING_SSH_HOST_PUBLIC_KEY_MD5],
two->data->set.str[STRING_SSH_HOST_PUBLIC_KEY_MD5]) &&
Curl_safecmp(one->data->set.str[STRING_SSH_HOST_PUBLIC_KEY_SHA256],
two->data->set.str[STRING_SSH_HOST_PUBLIC_KEY_SHA256]);
}
```
## Impact
## Summary:
An attacker who can perform a MITM against an initial unverified SFTP/SCP connection gains persistent access to all subsequent transfers on the same shared connection pool — including those from handles that explicitly require host key pinning via CURLOPT_SSH_KNOWNHOSTS or CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256. The strict handle's SSH handshake never occurs; it inherits the compromised connection silently, with no error returned to the application. Sensitive file contents, credentials, and configuration transferred over SFTP/SCP are exposed to the attacker despite the developer having configured server identity verification.
Actions
View on HackerOneReport Stats
- Report ID: 3640932
- State: Closed
- Substate: not-applicable