curl · Report #3826843
ssh_config_matches is dead code: unauthorized SSH key reuse
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 contain3e9817cd1. - 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".