Vulnerability DatabaseGHSA-qcf4-j55x-72hv

GHSA-qcf4-j55x-72hv: 
Ruby vulnerability analysis and mitigation

Patches

Fixed in net-ssh 8.1.2 and 7.3.10. Net::SSH::Proxy::HTTPS now verifies the proxy's certificate against the system trust store and checks its hostname by default, and checks the hostname with any verifying :ssl_context.

Workarounds

Upgrade. On older versions, passing an :ssl_context built with set_params verifies the certificate chain but not the hostname. After upgrading, a proxy with a self-signed or private CA certificate needs an :ssl_context whose cert_store trusts it; one with verify_mode = OpenSSL::SSL::VERIFY_NONE restores the old unverified behaviour.

Summary

Net::SSH::Proxy::HTTPS exists specifically so that proxy authentication credentials are sent over an encrypted channel rather than in plaintext (per its own class documentation: "This has the advantage that proxy authentication information is not sent in plaintext."). However, when a caller does not pass an explicit :ssl_context, the class builds a bare OpenSSL::SSL::SSLContext.new. That default context has verify_mode == VERIFY_NONE, no trust store, and no hostname verification. The code then calls SSLSocket#connect on it. As a result the TLS handshake to the proxy accepts any certificate, including a self-signed certificate issued for an unrelated hostname. A network attacker able to intercept the connection to the proxy (the classic threat model TLS is meant to defend against) can therefore impersonate the proxy, complete the handshake, and capture the Proxy-Authorization: Basic credentials that the client sends in the CONNECT request. Because the class's documented purpose is to keep those credentials confidential, the insecure default silently defeats the one guarantee the class offers.

Details

Weakness: CWE-295 (Improper Certificate Validation) Component: net-ssh (RubyGem), class Net::SSH::Proxy::HTTPS Affected file: lib/net/ssh/proxy/https.rb Confirmed in version: 7.3.6 (latest at time of writing) Severity (reporter estimate): Medium — see "Severity" below Status: Verified with a working, self-contained PoC

Vulnerable code

lib/net/ssh/proxy/https.rb:

def initialize(proxy_host, proxy_port = 80, options = {})
  @ssl_context = options.delete(:ssl_context) ||
                   OpenSSL::SSL::SSLContext.new   # <-- VERIFY_NONE, no trust store, no hostname check
  super(proxy_host, proxy_port, options)
end
def establish_connection(connect_timeout)
  plain_socket = super(connect_timeout)
  OpenSSL::SSL::SSLSocket.new(plain_socket, @ssl_context).tap do |socket|
    socket.extend(SSLSocketCompatibility)
    socket.connect                                # <-- no post_connection_check(hostname)
  end
end

Two independent gaps:

  1. The default context never sets verify_mode, so OpenSSL defaults to VERIFY_NONE and the certificate chain is not validated at all.
  2. Even when a caller does supply a VERIFY_PEER context, the code never sets the SNI hostname (SSLSocket#hostname=) and never calls post_connection_check(proxy_host), so hostname validation is not performed. (The PoC demonstrates gap #1; gap #2 is noted for completeness and would also warrant a fix.)

Severity

Reporter estimate: Medium. Rationale:

  • Requires a man-in-the-middle position on the connection to the proxy (raises attack complexity), and
  • Only affects callers who do not supply their own :ssl_context. But it is a genuine insecure default that silently nullifies the exact protection the class advertises, and the fix is low-risk, so it should not be dismissed as purely configuration. CVSS 3.1 (reporter estimate): AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:L/A:N → 5.9 (Medium). Maintainers should adjust to their own threat model.

Suggested remediation

Make the default context verify by default, and perform hostname verification:

def initialize(proxy_host, proxy_port = 80, options = {})
  @ssl_context = options.delete(:ssl_context) || begin
    ctx = OpenSSL::SSL::SSLContext.new
    ctx.set_params  # sets verify_mode = VERIFY_PEER and loads the default CA store
    ctx
  end
  super(proxy_host, proxy_port, options)
end
def establish_connection(connect_timeout)
  plain_socket = super(connect_timeout)
  OpenSSL::SSL::SSLSocket.new(plain_socket, @ssl_context).tap do |socket|
    socket.hostname = proxy_host           # SNI + enables hostname check
    socket.extend(SSLSocketCompatibility)
    socket.connect
    socket.post_connection_check(proxy_host)
  end
end

This is a behavior change (previously-working MITM'd or self-signed setups will start failing), so it is appropriate for a minor/major release with a changelog note. Callers who genuinely need the old behavior can still pass an :ssl_context with verify_mode = VERIFY_NONE explicitly, making the choice opt-in and visible rather than a silent default.

Proof of concept

A complete, self-contained PoC (poc/poc_verified.rb) was supplied privately to the maintainers. It runs entirely on 127.0.0.1 with no external network access:

  1. It generates a throwaway self-signed certificate for CN=evil-mitm.local (a hostname the "client" is not connecting to).
  2. It starts a local TLS listener (the rogue proxy) presenting that certificate.
  3. It drives the real library entry point, Net::SSH::Proxy::HTTPS#open('internal-ssh-host.example.com', 22), pointed at the local listener with :user/:password set but no :ssl_context.
  4. It prints whatever the rogue proxy received. Run:
gem install net-ssh      # tested against 7.3.6
ruby poc_verified.rb

Observed output

net-ssh 7.3.6  |  OpenSSL 3.5.5 27 Jan 2026
Default OpenSSL::SSL::SSLContext.new verify_mode => 0 (0/nil == VERIFY_NONE)
== DEFAULT CONFIG (no :ssl_context) — VULNERABLE ==
  client: open() SUCCEEDED (handshake accepted)
  >>> MITM captured proxy credentials: "victim:hunter2-proxy-secret"
== CONTROL (explicit VERIFY_PEER) — expected to fail ==
  client: open() raised OpenSSL::SSL::SSLError: ... certificate verify failed (self-signed certificate)
  endpoint saw: server-side error: ... tlsv1 alert unknown ca (SSL alert number 48)

The default-config run completes the handshake against a self-signed certificate for the wrong hostname and the rogue endpoint recovers the Base64-decoded credentials victim:hunter2-proxy-secret. The control run shows that enabling VERIFY_PEER correctly rejects the same certificate, confirming the behavior is caused by the missing verification and not by the PoC harness.

Impact

An attacker in a man-in-the-middle position on the path to the HTTPS proxy can:

  • Impersonate the proxy by presenting any certificate, and
  • Capture the proxy's HTTP Basic authentication credentials sent in the CONNECT exchange. This directly defeats the class's stated security goal. Depending on how the proxy credentials are used elsewhere, captured credentials may enable further access. An attacker who both captures the credentials and relays the tunnel can also observe timing/metadata of the tunneled traffic. Note on scope: the SSH session carried inside the tunnel is still protected by SSH host-key verification, so this issue alone does not grant SSH session takeover unless host-key checking is separately disabled or set to accept-new on first use. The confirmed, direct impact is disclosure of the proxy credentials and loss of proxy authenticity.

Source: NVD

Related Ruby vulnerabilities:

CVE ID

Severity

Score

Technologies

Component name

CISA KEV exploit

Has fix

Published date

GHSA-hjwv-j52r-xcg7HIGH7.4
  • Ruby logoRuby
  • net-ssh
NoYesNov 08, 2026
GHSA-qcf4-j55x-72hvMEDIUM6.5
  • Ruby logoRuby
  • net-ssh
NoYesNov 08, 2026
GHSA-hjvq-xxqv-7jxhMEDIUM5.9
  • Ruby logoRuby
  • savon
NoYesNov 06, 2026
GHSA-57m8-w3r7-5p3wMEDIUM4
  • Ruby logoRuby
  • savon
NoYesNov 06, 2026
GHSA-6vgc-f9v5-8939MEDIUMN/A
  • Ruby logoRuby
  • metainspector
NoYesNov 06, 2026

Free Vulnerability Assessment

Benchmark your Cloud Security Posture

Evaluate your cloud security practices across 9 security domains to benchmark your risk level and identify gaps in your defenses.

Request assessment

Additional Wiz resources

Get a personalized demo

Ready to see Wiz in action?

"Best User Experience I have ever seen, provides full visibility to cloud workloads."
David EstlickCISO
"Wiz provides a single pane of glass to see what is going on in our cloud environments."
Adam FletcherChief Security Officer
"We know that if Wiz identifies something as critical, it actually is."
Greg PoniatowskiHead of Threat and Vulnerability Management