Skip to main content
CVE-2026-40194VulnerabilityPublished Updated

SSH2::get_binary_packet() using != instead of hash_equals() · Phpseclib Has a Variable-time HMAC Comparison

Affects phpseclib/phpseclibSource GCVE Database

0
Authoritative record:CVE-2026-40194· Low 3.7

Not known to be exploited · CVSS 3.7 Low : upgrade to 1.0.28 when convenient.

Data as of 27 Oct 2026, 00:07 UTC

CVSS 3.1 base score
3.7Low
How severe the flaw is on the 0–10 CVSS scale.
Patch
Available – fixed in 1.0.28, 2.0.53, 3.0.51
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N

Exploitation evidence

  1. Disclosed: evidence found.

    CVE published 10 Apr 2026

  2. Detectable: no evidence on record.

    No public nuclei template indexed

  3. Weaponised: no evidence on record.

    No public exploit code indexed in Metasploit or Exploit-DB

  4. Exploited in the wild: no evidence on record.

    Not in CISA KEV and no source reports exploitation

  5. Observed by OffSeq Mirage: no evidence on record.

    No attack traffic recorded by OffSeq Mirage sensors

A missing step is not evidence of safety. Sources: cve.org (opens in a new tab), the Metasploit, Exploit-DB and nuclei-templates index as of 26 Oct 2026.

What to do

Upgrade phpseclib/phpseclib to 1.0.28; phpseclib/phpseclib to 2.0.53; phpseclib/phpseclib to 3.0.51.

Affected software

ProductAffectedFixed in
Packagistphpseclib/phpseclibAffected
  • >=0.1.1 <1.0.28
Fixed in
  • 1.0.28
Packagistphpseclib/phpseclibAffected
  • >=2.0.0 <2.0.53
Fixed in
  • 2.0.53
Packagistphpseclib/phpseclibAffected
  • >=3.0.0 <3.0.51
Fixed in
  • 3.0.51

Check your own hosts for these packages with threat-finder (opens in a new tab), our free open-source scanner.

Description

# phpseclib SSH2: Variable-time comparison in HMAC verification ## Summary `phpseclib\Net\SSH2::get_binary_packet()` uses PHP's `!=` operator to compare a received SSH packet HMAC against the locally computed HMAC. `!=` on equal-length binary strings in PHP uses `memcmp()`, which short-circuits on the first differing byte. This is a real variable-time comparison (CWE-208), proven by scaling benchmarks. The finding is **Low severity (defense-in-depth)**, not Critical. Practical exploitation over the network is prevented by SSH's disconnect-on-MAC-failure behavior combined with per-connection session keys. The fix is a one-liner: replace `!=` with `hash_equals()`, which the codebase already uses in 9 other places. - **Target:** `phpseclib/phpseclib` - **File:** `phpseclib/Net/SSH2.php` - **Lines (master `e819a163c`):** 3405 and 3410 - **CWE:** CWE-208 (Observable Timing Discrepancy) - **CVSS v3.1:** 3.7 — `CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N` - **Severity:** Low (cryptographic hygiene / defense-in-depth) - **Affected branches:** `master`, `3.0`, `2.0`, `1.0` (all supported versions) ## Root cause `phpseclib/Net/SSH2.php` lines 3399-3415 (master at `e819a163c`): ```php if ($this->hmac_check instanceof Hash) { $reconstructed = !$this->hmac_check_etm ? pack('Na*', $packet->packet_length, $packet->plain) : substr($packet->raw, 0, -$this->hmac_size); if (($this->hmac_check->getHash() & "\xFF\xFF\xFF\xFF") == 'umac') { $this->hmac_check->setNonce("\0\0\0\0" . pack('N', $this->get_seq_no)); if ($hmac != $this->hmac_check->hash($reconstructed)) { // <-- line 3405 $this->disconnect_helper(DisconnectReason::MAC_ERROR); throw new ConnectionClosedException('Invalid UMAC'); } } else { if ($hmac != $this->hmac_check->hash(pack('Na*', $this->get_seq_no, $reconstructed))) { // <-- line 3410 $this->disconnect_helper(DisconnectReason::MAC_ERROR); throw new ConnectionClosedException('Invalid HMAC'); } } } ``` Both `$hmac` (read from the socket via `Strings::pop($raw, $this->hmac_size)` at line 3348) and the computed hash are equal-length binary strings. PHP's `!=` operator on equal-length strings dispatches to `zend_binary_strcmp()` which internally calls `memcmp()`. Modern libc `memcmp` short-circuits on the first differing byte, producing a timing signal that scales linearly with the number of matching leading bytes. ### The same bug exists on every supported branch | Branch | File | Line(s) | Expression | |---|---|---|---| | `master` (@ `e819a163c`) | `phpseclib/Net/SSH2.php` | 3405, 3410 | `$hmac != $this->hmac_check->hash(...)` | | `3.0` | `phpseclib/Net/SSH2.php` | 3741, 3746 | `$hmac != $this->hmac_check->hash(...)` | | `2.0` | `phpseclib/Net/SSH2.php` | 3796 | `$hmac != $this->hmac_check->hash(...)` | | `1.0` | `phpseclib/Net/SSH2.php` | 3810 | `$hmac != $this->hmac_check->hash(...)` | Verified with `git show <branch>:phpseclib/Net/SSH2.php`. ### Reachability The HMAC verification path at lines 3399-3415 is reached on **every received SSH packet** when the negotiated cipher is not AEAD — i.e., any of: - `aes128-cbc`, `aes192-cbc`, `aes256-cbc` - `aes128-ctr`, `aes192-ctr`, `aes256-ctr` - `3des-cbc`, `3des-ctr` - `blowfish-cbc`, `blowfish-ctr` - `twofish-*-cbc`, `twofish-*-ctr` - `arcfour`, `arcfour128`, `arcfour256` combined with any non-AEAD MAC (hmac-sha2-256, hmac-sha2-512, hmac-sha1, hmac-sha1-96, hmac-md5, hmac-md5-96, umac-64, umac-128, and their -etm variants — full list at `getSupportedMACAlgorithms()` around line 4754). AEAD ciphers (`aes128-gcm@openssh.com`, `aes256-gcm@openssh.com`, `chacha20-poly1305@openssh.com`) go through a different path (lines 3353-3381) and do not reach the `!=` comparison — their authentication tag is checked inside the AEAD implementation. `$this->hmac_check` is set during key exchange at `SSH2.php:1812`: ```php if (!$this->decrypt->usesNonce()) { [$this->hmac_check, $checkKeyLength] = self::mac_algorithm_to_hash_instance($mac_algorithm_in); $this->hmac_size = $this->hmac_check->getLengthInBytes(); } ``` So any SSH2 client session negotiating a non-AEAD cipher exercises the vulnerable code path starting from the first post-KEX packet. ### Contrast with existing code that does the right thing `hash_equals()` is already used in 9 other places in the phpseclib codebase, proving the maintainer knows the pattern: ``` phpseclib/File/CMS/DigestedData.php:186 phpseclib/File/CMS/SignedData/Signer.php:294 phpseclib/File/CMS/SignedData/Signer.php:406 phpseclib/Crypt/Common/Formats/Keys/PuTTY.php:236 phpseclib/Crypt/RSA/PublicKey.php:98, 105, 188, 229 phpseclib/Crypt/RSA/PrivateKey.php:386 ``` The SSH2 MAC comparison is the one place it was missed. ## Proof of concept All scripts under `poc/`. They prove three things: 1. PHP's `!=` on equal-length binary strings IS variable-time and short-circuits. 2. `hash_equals()

References

Community reviews

Loading reviews…