Skip to content

url: SSH host-verification options silently unused on connection reuse #21606

@MegaManSec

Description

@MegaManSec

I did this

This is a follow-up to 3e9817cd1b (2026-05-07, "url: remove ssh_config_matches"). Not a security report — I understand the documented rationale: host verification happens at connect time, the verified connection persists, use FORBID_REUSE if you need per-transfer checks. The design is coherent and the commit message explains it clearly.

The concern is narrower: CURLOPT_SSH_HOSTKEYFUNCTION. The SHA256/MD5/KNOWNHOSTS options are static values checked once against a host key — skipping them on reuse is fine, the host was already accepted. But CURLOPT_SSH_HOSTKEYFUNCTION is a callback. Callers set it to run active logic on each transfer: audit logging, TOFU state updates, revocation checks. On a reused connection the callback simply doesn't fire, and there is no signal in the return code that this happened — the transfer returns CURLE_OK and the caller has no way to know their callback was skipped.

Quick reproducer: two easies on one multi, same URL/user/password, only the SHA256 pin differs:

eA: CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256 = "gWi8KeA9...GMA"    (correct)
eB: CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256 = "AAAABBBBCCCC...KKK" (wrong)

Run A to completion, remove from multi (keep conn in pool), add B, run B:

========== A: sftp://.../file1 fp=CORRECT ==========
* SHA256 checksum match
* Authentication complete

========== B: sftp://.../file2 fp=WRONG ==========
* Reusing existing sftp: connection with host 127.0.0.1

A: CURLE=0
B: CURLE=0   /file2 body: 'file2 contents'

No SHA256 fingerprint: line for B; the wrong pin sits unread in B.data->set.str[...]. Server log shows one TCP accept, one auth, two file opens. Same shape with _MD5, _KNOWNHOSTS, _AUTH_TYPES, and _HOSTKEYFUNCTION.

The doc update in c31fcf2dec reads like the single-handle "I changed my mind" scenario — the wording could be a bit sharper about multi-handle pools so callers who set CURLOPT_SSH_HOSTKEYFUNCTION for per-transfer side-effects know they need FORBID_REUSE. For the callback specifically, refusing reuse outright when either side has ssh_hostkeyfunc set would be the most conservative option and avoids needing any doc clarification.

I expected the following

docs are readable to coincide with the actual behavior

curl/libcurl version

master

operating system

all

Metadata

Metadata

Assignees

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions