HexBrain

curl · Report #3826843

ssh_config_matches is dead code: unauthorized SSH key reuse

Informativestatus
mediumseverity
11upvotes
30 Jun 2026disclosed

How it was found

Summary

libcurl's SSH connection-reuse guard ssh_config_matches() — added for CVE-2022-27782 and reaffirmed by CVE-2023-27538 — is dead code in every release since 7.83.1. It compares sshc->rsa / sshc->rsa_pub between a new transfer ("needle") and a pooled connection, but on both sides those pointers are NULL at the moment the comparison runs, so Curl_safecmp(NULL, NULL) always returns TRUE and the guard always lets the reuse through. On the needle side, sshc is curlx_calloc'd in ssh_setup_connection() before ConnectionExists() runs, so the keyfile pointers are still NULL at reuse-check time; on the pooled side, sshc->rsa / rsa_pub are freed unconditionally right after libssh2_userauth_publickey_fromfile_ex(), before the rc == 0 check. As a result, a transfer configured with an unauthorized SSH key reuses a pooled session authenticated with a different, authorized key, and the server serves the data without ever seeing the unauthorized key.

This is the complement to HackerOne report #3788506, which reported the removal of ssh_config_matches on master (3e9817cd1b, 2026-05-07) and was closed on shipping grounds — the removal was reverted by 0618ffe50d before any release. The deeper issue #3788506 did not address is that the guard has been dead code in every release since 7.83.1: present, compiled, called, but always returning TRUE. CVE-2023-27538's advisory already states the CVE-2022-27782 fix "was bad and did not actually correct the problem"; CVE-2023-27538 only fixed a bitmask so the dead guard finally got called for SFTP/SCP — it left the guard itself dead. The advisories claim curl >= 7.83.1 / curl >= 8.0.0 are not affected; they are.

I reproduced this on curl 8.20.0, 8.21.0, and master (libssh2 backend) with a real sshd: an unauthorized key reads a secret by reusing an authorized key's pooled session; Step 3 (fresh pool) confirms keyB is genuinely rejected by the server. A self-contained Docker verification environment is in ssh-reuse-deadcode/. The 3-step PoC:

Step 1: keyA (authorized)   GET sftp://.../secret  -> OK      (leaves conn in pool)
Step 2: keyB (unauthorized) GET same, same pool    -> OK      <-- BUG: reuses keyA's session
Step 3: keyB (unauthorized) GET same, FRESH pool   -> DENIED  (control: keyB truly rejected)

VULNERABILITY CONFIRMED iff Step 1 OK and Step 2 OK and Step 3 DENIED.

Affected version

Reproduced on (libssh2 backend, SFTP enabled, debian:trixie-slim container):

  • curl 8.20.0 — commit a05f34973e (released 2026-04-29). Does not contain 3e9817cd1.
  • curl 8.21.0 — commit 68720b4837 (current release, 2026-06-24).
  • master — commit a36384ab94 (8.21.1-DEV, tested 2026-06-26).

All three versions produce VULNERABILITY CONFIRMED. The dead-code pattern holds in source from curl-7_83_1 (the release that shipped the CVE-2022-27782 fix) through the current release.

Steps To Reproduce

A self-contained verification environment lives in ssh-reuse-deadcode/ (see its README.md for full details). It builds curl from upstream inside a container, starts a real sshd, and runs the 3-step SFTP test. keyA is placed in authorized_keys; keyB is deliberately not. No host source tree is mutated.

cd ssh-reuse-deadcode
./verify.sh                  # all 3 versions (8.20.0, 8.21.0, master)
./verify.sh 8.20.0           # one version

Expected vulnerable output

Client verdict (stdout) — Step 2 reads the same secret as Step 1:

>> Step1 result=0 (No error) body='user_a_secret_data'     <- keyA reads secret
>> Step2 result=0 (No error) body='user_a_secret_data'     <- keyB reads same secret (BUG)
>> Step3 result=67 (Login denied) body=''                   <- fresh pool, keyB rejected

Step 1 (keyA, authorized)  : OK
Step 2 (keyB, shared pool) : SUCCEEDED  <-- BUG
Step 3 (keyB, fresh pool)  : DENIED (control)

VULNERABILITY CONFIRMED: unauthorized keyB read the secret by reusing
keyA's pooled SSH session.

sshd VERBOSE log (only keyA is ever accepted; Step 2 opens no new connection):

Accepted publickey for root ... RSA SHA256:<keyA>     <- Step 1
Failed publickey  for root ... RSA SHA256:<keyB>      <- Step 3 only

curl verbose (Step 2 shows no Trying, no authenticated via publickey):

== Step 1: authorized keyA ==
*   Trying 127.0.0.1:12222...
* SSH: authenticated via publickey
== Step 2: UNAUTHORIZED keyB (shared pool) ==
* Reusing existing sftp: connection with host 127.0.0.1   <- no auth
== Step 3: UNAUTHORIZED keyB (FRESH pool, control) ==
*   Trying 127.0.0.1:12222...
* SSH public key authentication failed

Reproduction across versions

./verify.sh (all three versions) produces:

  VERSION  COMMIT                                        VERDICT
  -------  --------------------------------------------  --------
  8.20.0   a05f34973e6c4bb629d018f7cb51487be1c904d8     CONFIRMED (vulnerable)
  8.21.0   68720b4837284335b2d63cb358f8f6ce65f5bc55     CONFIRMED (vulnerable)
  master   a36384ab94b868b70b39209ced3e89927a14922d     CONFIRMED (vulnerable)

Full logs (including sshd auth log and curl verbose output) are saved to results/<version>.log.

████████

████████

Impact

Summary:

A transfer configured with an unauthorized SSH key reads or writes data on the SSH server as if it were the authorized key. The unauthorized key is never presented to the server, so server-side authorization provides no defense.

Affected: any application sharing a libcurl connection cache (CURLOPT_SHARE + CURL_LOCK_DATA_CONNECT, or a reused CURLM/easy handle) across SFTP/SCP transfers with different per-transfer SSH keys — multi-tenant SFTP gateways, CI/CD runners, backup/sync daemons.

This is CWE-305: Authentication Bypass by Primary Weakness — curl's own classification for CVE-2022-27782 (Medium) and CVE-2023-27538 (downgraded to Low on the premise the path was fixed; it was not). Reproduced on 8.20.0, 8.21.0, and master, all >= 8.0.0 and claimed "not affected".

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