phpseclib has a variable-time HMAC comparison in SSH2::get_binary_packet() using != instead of hash_equals()
# phpseclib SSH2: Variable-time comparison in HMAC verification
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.
phpseclib/phpseclibphpseclib/Net/SSH2.phpe819a163c): 3405 and 3410CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:Nmaster, 3.0, 2.0, 1.0 (all supported versions)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.
| 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.
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-cbcaes128-ctr, aes192-ctr, aes256-ctr3des-cbc, 3des-ctrblowfish-cbc, blowfish-ctrtwofish-*-cbc, twofish-*-ctrarcfour, arcfour128, arcfour256combined 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 ([email protected], [email protected], [email protected]) 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.
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.
All scripts under poc/. They prove thr
This issue can be reached over the network, attack complexity is high, an attacker needs no privileges on the target. No user interaction is required. The scope is unchanged, so the impact stays within the vulnerable component. Rated impact: confidentiality low, integrity none, availability none.
The score comes from this vector: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N
CVE-2026-40194 is classified as CWE-208: Observable Timing Discrepancy. Two separate operations in a product require different amounts of time to complete, in a way that is observable to an actor and reveals security-relevant information about the state of the product, such as whether a particular operation was successful or not.
CVE-2026-40194 is recorded against 2 packages.
Published on 10 April 2026 and last revised on 17 June 2026. No public exploit is currently recorded for this entry. A vendor advisory or fix has been published. Record sourced from NVD.
github.com
github.com
github.com
github.com
github.com
github.com
phpseclib has other advisories on record. If you are patching this one, these are worth checking on the same host:
These advisories are the same class of weakness (CWE-208: Observable Timing Discrepancy) in other software:
Each distribution ships its own build and its own fixed version. Pick the one you run:
Details
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N
Affected Packages
| Software | From version | Fixed in |
|---|---|---|
| phpseclib | 3.0.0 | 3.0.51 |
| phpseclib/phpseclib | 3.0.0 | 3.0.51 |
References
Similar Threats
Free Vulnerability Check
BotEraser helps you identify potentially vulnerable plugins and themes by checking your installation against CVE-2026-40194 and other known CVE records.
Scan My Site Free →No credit card required · Results in minutes
ⓘ Data Notice: The information presented above has been compiled from publicly available internet sources. Boteraser aggregates this data solely for informational purposes and does not independently classify, evaluate, or endorse any findings about the vulnerabilities listed. The accuracy and completeness of this information is the sole responsibility of the original publishers. Boteraser and its operators accept no liability for any decisions made based on this data.
Stay up to date with the latest from Boteraser.
We use cookies to improve your experience on our site. By using our site, you consent to cookies.
Manage your cookie preferences below:
Essential cookies enable basic functions and are necessary for the proper function of the website.
CloudFlare provides web performance and security solutions, enhancing site speed and protecting against threats.
Service URL: developers.cloudflare.com (opens in a new window)
These cookies are needed for adding comments on this website.
These cookies are used for managing login functionality on this website.
Statistics cookies collect information anonymously. This information helps us understand how visitors use our website.
Google Analytics is a powerful tool that tracks and analyzes website traffic for informed marketing decisions.
Service URL: policies.google.com (opens in a new window)
You can find more information in our Cookie Policy and Privacy Policy.