HexBrain

curl · Report #3355218

CVE-2025-10966: missing SFTP host verification with wolfSSH

Validstatus
lowseverity
43upvotes
5 Nov 2025disclosed

How it was found

Summary:

When curl is built with the wolfSSH backend, the SSH/SFTP implementation in lib/vssh/wolfssh.c performs no server host key verification and exposes no host identity options in the curl tool. I verified this locally by building curl with wolfSSH (binary reports wolfssh/1.4.20), observing that the SSH host verification options are not available in the tool, and inspecting the wolfSSH code paths that connect without any host key check or known_hosts handling.

AI usage statement: This report text was prepared with assistance, but the findings below were verified manually via local build, runtime behavior, and code inspection.

Affected version

Local build on macOS (Apple Silicon):

$ ./src/curl --version
curl 8.17.0-DEV (aarch64-apple-darwin23.3.0) libcurl/8.17.0-DEV wolfSSL/5.8.2 zlib/1.2.12 brotli/1.1.0 zstd/1.5.7 libidn2/2.3.8 libpsl/0.21.5 wolfssh/1.4.20 nghttp2/1.66.0 librtmp/2.3
Protocols: dict file ftp ftps gopher gophers http https imap imaps ipfs ipns ldap ldaps mqtt pop3 pop3s rtmp rtsp sftp smb smbs smtp smtps telnet tftp ws wss

Steps To Reproduce:

  1. Build wolfSSH (with SFTP) and install locally (macOS/Homebrew shown):

    # Prereqs (Homebrew)
    brew install wolfssl libpsl libidn2 libnghttp2
    
    # Build and install wolfSSH with SFTP enabled
    git clone --depth=1 https://github.com/wolfSSL/wolfssh.git
    cd wolfssh
    ./autogen.sh
    ./configure \
      --prefix=$HOME/.local/wolfssh \
      --with-wolfssl=/opt/homebrew \
      --enable-sftp \
      --enable-sshclient \
      --enable-examples \
      CPPFLAGS='-I/opt/homebrew/include' \
      LDFLAGS='-L/opt/homebrew/lib'
    make -j$(sysctl -n hw.ncpu)
    make install
    
  2. Configure and build curl with wolfSSH:

    cd /Users/$USER/scanner-repos/curl
    autoreconf -fi
    PKG_CONFIG_PATH="$HOME/.local/wolfssh/lib/pkgconfig:/opt/homebrew/lib/pkgconfig" \
    CPPFLAGS="-I$HOME/.local/wolfssh/include -I/opt/homebrew/include" \
    LDFLAGS="-L$HOME/.local/wolfssh/lib -L/opt/homebrew/lib" \
    ./configure \
      --without-libssh2 \
      --without-libssh \
      --with-wolfssh=$HOME/.local/wolfssh \
      --with-wolfssl=/opt/homebrew \
      --disable-shared
    make -j$(sysctl -n hw.ncpu)
    
  3. Verify the build shows wolfSSH:

    ./src/curl --version
    # Output includes: wolfssh/1.4.20
    
  4. Observe that SSH host identity options are unavailable in this build:

    ./src/curl --help ssh
    # No --ssh-knownhosts or --hostpubsha256 listed
    
    ./src/curl --ssh-knownhosts /tmp/kh -vvv sftp://localhost:22/
    # Error observed:
    # curl: option --ssh-knownhosts: is unknown
    
    ./src/curl -v --hostpubsha256 AAAA sftp://demo:[email protected]/readme.txt
    # Error observed:
    # curl: option --hostpubsha256: the installed libcurl version does not support this
    

    These runtime observations match the wolfSSH backend’s code, which does not implement host key verification or known_hosts integration.

  5. Code references in lib/vssh/wolfssh.c and lib/vssh/ssh.h (exact lines):

    • Connection setup: no host key verification callback or known_hosts loading, then immediately enters the state machine.
static CURLcode wssh_connect(struct Curl_easy *data, bool *done)
{
  struct connectdata *conn = data->conn;
  struct ssh_conn *sshc = Curl_conn_meta_get(conn, CURL_META_SSH_CONN);
  struct SSHPROTO *sshp = Curl_meta_get(data, CURL_META_SSH_EASY);
  curl_socket_t sock = conn->sock[FIRSTSOCKET];
  int rc;

  if(!sshc || !sshp)
    return CURLE_FAILED_INIT;

  /* We default to persistent connections. We set this already in this connect
     function to make the reuse checks properly be able to check this bit. */
  connkeep(conn, "SSH default");

  if(conn->handler->protocol & CURLPROTO_SCP) {
    conn->recv[FIRSTSOCKET] = wscp_recv;
    conn->send[FIRSTSOCKET] = wscp_send;
  }
  else {
    conn->recv[FIRSTSOCKET] = wsftp_recv;
    conn->send[FIRSTSOCKET] = wsftp_send;
  }
  sshc->ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL);
  if(!sshc->ctx) {
    failf(data, "No wolfSSH context");
    goto error;
  }

  sshc->ssh_session = wolfSSH_new(sshc->ctx);
  if(!sshc->ssh_session) {
    failf(data, "No wolfSSH session");
    goto error;
  }

  rc = wolfSSH_SetUsername(sshc->ssh_session, conn->user);
  if(rc != WS_SUCCESS) {
    failf(data, "wolfSSH failed to set username");
    goto error;
  }

  /* set callback for authentication */
  wolfSSH_SetUserAuth(sshc->ctx, userauth);
  wolfSSH_SetUserAuthCtx(sshc->ssh_session, data);

  rc = wolfSSH_set_fd(sshc->ssh_session, (int)sock);
  if(rc) {
    failf(data, "wolfSSH failed to set socket");
    goto error;
  }

#if 0
  wolfSSH_Debugging_ON();
#endif

  *done = TRUE;
  if(conn->handler->protocol & CURLPROTO_SCP)
    wssh_state(data, sshc, SSH_INIT);
  else
    wssh_state(data, sshc, SSH_SFTP_INIT);

  return wssh_multi_statemach(data, done);
error:
  wssh_sshc_cleanup(sshc);
  return CURLE_FAILED_INIT;
}
  • State machine: after wolfSSH_connect() succeeds, it transitions directly to SSH_STOP without any SSH_HOSTKEY verification step.
case SSH_S_STARTUP:
  rc = wolfSSH_connect(sshc->ssh_session);
  if(rc != WS_SUCCESS)
    rc = wolfSSH_get_error(sshc->ssh_session);
  if(rc == WS_WANT_READ) {
    *block = TRUE;
    conn->waitfor = KEEP_RECV;
    return CURLE_OK;
  }
  else if(rc == WS_WANT_WRITE) {
    *block = TRUE;
    conn->waitfor = KEEP_SEND;
    return CURLE_OK;
  }
  else if(rc != WS_SUCCESS) {
    wssh_state(data, sshc, SSH_STOP);
    return CURLE_SSH;
  }
  infof(data, "wolfssh connected");
  wssh_state(data, sshc, SSH_STOP);
  break;
  • The SSH_HOSTKEY state exists in the common SSH state enumeration but is not used by the wolfSSH state machine above.
typedef enum {
  SSH_NO_STATE = -1,  /* Used for "nextState" so say there is none */
  SSH_STOP = 0,       /* do nothing state, stops the state machine */

  SSH_INIT,           /* First state in SSH-CONNECT */
  SSH_S_STARTUP,      /* Session startup */
  SSH_HOSTKEY,        /* verify hostkey */
  SSH_AUTHLIST,
  SSH_AUTH_PKEY_INIT,
  SSH_AUTH_PKEY,
  SSH_AUTH_PASS_INIT,
  SSH_AUTH_PASS,
  SSH_AUTH_AGENT_INIT, /* initialize then wait for connection to agent */
  SSH_AUTH_AGENT_LIST, /* ask for list then wait for entire list to come */
  SSH_AUTH_AGENT,      /* attempt one key at a time */
  SSH_AUTH_HOST_INIT,
  SSH_AUTH_HOST,
  SSH_AUTH_KEY_INIT,
  SSH_AUTH_KEY,
  SSH_AUTH_GSSAPI,
  SSH_AUTH_DONE,
  SSH_SFTP_INIT,
  SSH_SFTP_REALPATH,   /* Last state in SSH-CONNECT */

Supporting Material/References:

  • Tool help and error outputs from the local wolfSSH build:
$ ./src/curl --help ssh
ssh: SSH protocol
     --compressed-ssh          Enable SSH compression
     --hostpubmd5 <md5>        Acceptable MD5 hash of host public key
     --hostpubsha256 <sha256>  Acceptable SHA256 hash of host public key
 -k, --insecure                Allow insecure server connections
     --key <key>               Private key filename
     --pass <phrase>           Passphrase for the private key
     --pubkey <key>            SSH Public key filename
$ ./src/curl --ssh-knownhosts /tmp/kh -vvv sftp://localhost:22/
curl: option --ssh-knownhosts: is unknown
$ ./src/curl -v --hostpubsha256 AAAA sftp://demo:[email protected]/readme.txt
curl: option --hostpubsha256: the installed libcurl version does not support this

These observations, together with the wolfSSH connection and state machine code above, show that host identity verification is not implemented for this backend.

Impact

Summary:

If curl/libcurl is built with the wolfSSH backend and used for SFTP, server host identities are not verified. This permits potential man‑in‑the‑middle attacks on SSH/SFTP connections in environments using the wolfSSH backend, since any presented server host key is effectively accepted by the client (no known_hosts or fingerprint enforcement, and no hostkey verification step in the state machine).

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