Common Weakness Enumeration

CWE-208

Allowed

Observable Timing Discrepancy

Abstraction: Base · Status: Incomplete

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.

394 vulnerabilities reference this CWE, most recent first.

GHSA-Q97C-8QH3-FPC6

Vulnerability from github – Published: 2026-09-08 21:24 – Updated: 2026-09-29 21:15
VLAI
Summary
phpseclib — non-constant-time X25519 scalar multiplication permits full private-key recovery
Details

The pure-PHP X25519 scalar multiplication in phpseclib is not constant-time. Field addition and subtraction each perform a data-dependent conditional modular reduction, so the cost of each Montgomery-ladder step is a linear function of that step's reduction count which is a quantity determined by the secret scalar's prefix.

An observer with per-ladder-step resolution recovers the 251-bit clamped private scalar. This is a per-step leak, not an aggregate one: an instrumented code proof-of-concept recovers 20/20 test keys from 32 observed operations, and an observer that counts libgmp calls instead of timing them recovers a key from a single operation.

This is not a low-order-input issue. Recovery works with the RFC 7748 base point u = 9, with no attacker-chosen input at all. Rejecting low-order public values does not close it.

2. Affected component

Confirmed on phpseclib 3.0.56 (338 files under phpseclib/,sha256(sorted(relpath NUL file_sha256 LF)) = cc7250b611f520e809131aab0931503457c44d8cbfb10d535251c6fec5f62a2b). The code appears unchanged across the 3.0 series wherever Curve25519 is supported, please confirm the affected range.

file:line role
Math/PrimeField/Integer.php:189 add() — conditional subtract($modulo) when the sum ≥ p
Math/PrimeField/Integer.php:207 subtract() — conditional add($modulo) when the result is negative
Crypt/EC/BaseCurves/Montgomery.php:229–234 ladder branch on the secret bit, selecting argument order of doubleAndAddPoint
Crypt/EC/Formats/Keys/MontgomeryPrivate.php:66 multiplyPoint(getBasePoint(), dA) — no engine check of any kind
Crypt/EC/Formats/Keys/PKCS8.php:194–200 the same derivation, correctly gated on ext-sodium — the pattern MontgomeryPrivate is missing

3. Technical description

Operation counts in the ladder are already constant — 10 field multiplications, 4 additions and 4 subtractions per step, 2560 multiplications per 256-step ladder. Operand values are not. Each PrimeField\Integer::add() / subtract() takes a data-dependent branch costing ~0.85–1.0 µs on the GMP engine, against a ~32 µs step period, so per-step cost is α + β·c where c is that step's conditional-reduction count. Measured across 20 keys: R² = 0.91–0.98, β = 838–920 ns.

c depends on the whole scalar prefix, not on the current bit, so per-step thresholding is useless — it saturates at ~93% per bit for u = p−1 and at chance for u = 9, and recovers 0/20 keys either way, because the bit string is a prefix-XOR in which one flipped step inverts the entire tail. Conditioning on the prefix removes the ambiguity: a beam search replays both branches from each candidate ladder state, reads off the exact c for each, and scores against the observation. The victim's public key adjudicates the small residual search.

Two facts bound the problem and are worth stating precisely, because they determine whether a fix is needed at all:

  • Aggregate observation is provably useless. The adjacent-bit transition count T(k) has exact entropy H(T) = 4.0357 bits over clamped scalars, so a noiseless transition-count oracle still leaves ~2^247 candidates. The summed reduction count Σc is richer (~6.6–6.9 bits) and still leaves ~2^244. Any measurement that collapses the call to one number is safe. Per-step measurement is not.
  • The libgmp call counts are exactly determined. Per ladder step, __gmpz_add = 4 + csub, __gmpz_sub = 4 + cadd, __gmpz_mul = __gmpz_mod = 10. Verified by differencing gdb breakpoint counts against phpseclib's own doubleAndAddPoint — 27/27 steps exact, extended independently to 64/64 and 38/38 by our two reviewers. An observer that only counts these calls needs no timing, no calibration and no repetition.

Results, 20 keys × 3 sampling seeds, 800 traces per path collected from 800 distinct PHP processes (so the observations are cross-process, as real requests would be):

observer path observations needed exact 251-bit recovery
timing key load, u = 9 32 20/20 keys, 95% CI [83.9%, 100%]
timing ECDH, u = p−1 32 18/20 keys, 95% CI [69.9%, 96.8%]
timing either 8 18–23% of trials
libgmp call counts either 1 20/20 keys; tolerates 20–30% of per-step counts being wrong

The model underlying the decoder is validated against the pinned implementation: 254/254 (key, peer) outputs match the real DH::computeSecret, and all four RFC 7748 §6.1 vectors match both phpseclib and the published constants.

Negative controls are clean — wrong public key, shuffled trace, wrong peer value, foreign key: 0/20 in every case. Nothing derived from the private key reaches the decoder; its inputs are the observation vector, the peer value, the victim's public key, and the public clamping constants.

5. Impact

In the instrumented, local model, recovery of the clamped scalar gives a permanent compromise of the X25519 private key. Clamping is applied on every call, so the recovered value is what every past and future operation with that key uses.

6. Restrictions

Required for exploitation:

  1. A reused / long-lived X25519 private key. Ephemeral X25519 — the normal TLS and SSH case — defeats this outright. phpseclib's own SSH path generates a fresh scalar per exchange and is not affected.
  2. Knowledge of the victim's public key. It adjudicates the decoder's residual search; without it no candidate can be selected. This is normally public, but it is a precondition, not a convenience.
  3. The pure-PHP path must actually run. Measured across four extension configurations:
  4. EC::loadFormat('MontgomeryPrivate', $raw32) runs the ladder in every configuration — but the format declares IS_INVISIBLE (MontgomeryPrivate.php:40), so PublicKeyLoader::load skips it and nothing inside phpseclib calls it. An application must name the format explicitly.
  5. PKCS8 / PublicKeyLoader::load / EC::createKey run the ladder only when ext-sodium is absent — PKCS8.php:194 gates on sodium_crypto_box_publickey_from_secretkey. OpenSSL does not help here.
  6. DH::computeSecret runs the ladder only under EC::forceEngine('PHP'), or when both openssl_pkey_derive (DH.php:325) and sodium_crypto_scalarmult (EC/PrivateKey.php:75) are unavailable. ext-sodium is bundled and enabled by default in PHP 7.2+, so the reachable configurations are a minority — though disable_functions hardening and --disable-sodium builds do occur, particularly in shared hosting.
  7. An observer with per-ladder-step resolution, i.e. one that can distinguish ~0.9 µs within a ~32 µs step, or count libgmp entry-point calls. In practice that means local co-residency (e.g. a Flush+Reload spy on the shared libgmp.so mapping — __gmpz_add / __gmpz_sub are the correct targets; __gmpn_* are not, being size-dispatched internals).

Not demonstrated — stated so it is not found rather than disclosed:

  • We did not build a co-resident spy. Per-step observations in the PoC come from a small hrtime() hook inside Montgomery::multiplyPoint (one file differs from the pinned tree; Math/PrimeField/Integer.php, which carries the leak, is byte-identical). This models an observer with intra-call resolution; it is not itself an attacker capability. We note that the requisite primitives are present on ordinary hardware — on our test host clflush and rdtscp work, with 187–312 cycle cached/flushed separation — but building and validating a spy is separate work we have not done.
  • Perfect step segmentation is assumed. A real observer must recover step boundaries and separate the ladder from the surrounding modular inversion.
  • A single host and configuration. All timing figures are from one 2-vCPU shared VM (PHP 8.3.6, GMP 6.3.0). A noisier host needs more observations; recovery holds within roughly 25–30% additional noise beyond this host's residual.
  • We have not identified an affected deployed caller.

Accordingly we report this as a hardening issue and demonstrated side channel, not as a completed remote exploit.

8. Suggested remediation

  1. Constant-time, fixed-width field arithmetic, or delegate to a vetted native provider. This is the actual fix. Removing the ladder's bit branch is not sufficient while Integer.php:189 and :207 remain operand-dependent.
  2. Gate MontgomeryPrivate.php:66 the way PKCS8.php:194–200 already is. That is a one-block change and it closes the only entry point that is un-gated in every configuration. Keep the $curve instanceof Curve25519 guard — MontgomeryPrivate also accepts Curve448 keys.
  3. Consider an OpenSSL arm alongside the sodium arm in PKCS8::loadECDH, or fail closed, so stacks without ext-sodium do not fall through to the ladder.
  4. Separately, and unrelated to this channel: the pure-PHP path returns an all-zero 32-byte shared secret for low-order peer inputs. Rejecting the full canonicalised low-order set and adding a constant-time all-zero check is correct hygiene for contributory behaviour — but it does not mitigate the timing channel, since recovery works with u = 9.

Contact

George Stergiopoulos Assistant Professor of cybersecurity Athens University of Economics and Business, Greece E: geostergiop@aueb.gr | s: https://www.aueb.gr/en/faculty_page/stergiopoulos-georgios

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Packagist",
        "name": "phpseclib/phpseclib"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "3.0.57"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "Packagist",
        "name": "phpseclib/phpseclib"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "4.0.0"
            },
            {
              "fixed": "4.0.1"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-84308"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-208",
      "CWE-385"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-09-08T21:24:29Z",
    "nvd_published_at": "2026-09-01T20:17:24Z",
    "severity": "MODERATE"
  },
  "details": "The pure-PHP X25519 scalar multiplication in phpseclib is not constant-time. Field addition and subtraction each perform a **data-dependent conditional modular reduction**, so the cost of each Montgomery-ladder step is a linear function of that step\u0027s reduction count which is a quantity determined by the secret scalar\u0027s *prefix*.\n\nAn observer with per-ladder-step resolution recovers the 251-bit clamped private scalar. This is a per-step leak, not an aggregate one: an instrumented code proof-of-concept recovers 20/20 test keys from 32 observed operations, and an observer that counts libgmp calls instead of timing them recovers a key from a **single** operation.\n\nThis is **not** a low-order-input issue. Recovery works with the RFC 7748 base point `u = 9`, with no attacker-chosen input at all. Rejecting low-order public values does not close it.\n\n## 2. Affected component\n\nConfirmed on **phpseclib 3.0.56** (338 files under `phpseclib/`,`sha256(sorted(relpath NUL file_sha256 LF)) = cc7250b611f520e809131aab0931503457c44d8cbfb10d535251c6fec5f62a2b`).\nThe code appears unchanged across the 3.0 series wherever Curve25519 is supported, please confirm the affected range.\n\n| file:line | role |\n|---|---|\n| `Math/PrimeField/Integer.php:189` | `add()` \u2014 conditional `subtract($modulo)` when the sum \u2265 p |\n| `Math/PrimeField/Integer.php:207` | `subtract()` \u2014 conditional `add($modulo)` when the result is negative |\n| `Crypt/EC/BaseCurves/Montgomery.php:229\u2013234` | ladder branch on the secret bit, selecting argument order of `doubleAndAddPoint` |\n| `Crypt/EC/Formats/Keys/MontgomeryPrivate.php:66` | `multiplyPoint(getBasePoint(), dA)` \u2014 **no engine check of any kind** |\n| `Crypt/EC/Formats/Keys/PKCS8.php:194\u2013200` | the same derivation, correctly gated on ext-sodium \u2014 the pattern `MontgomeryPrivate` is missing |\n\n## 3. Technical description\n\nOperation *counts* in the ladder are already constant \u2014 10 field multiplications, 4 additions and 4 subtractions per step, 2560 multiplications per 256-step ladder. Operand *values* are not. Each `PrimeField\\Integer::add()` / `subtract()` takes a data-dependent branch costing ~0.85\u20131.0 \u00b5s on the GMP engine, against a ~32 \u00b5s step period, so per-step cost is `\u03b1 + \u03b2\u00b7c` where `c` is that step\u0027s conditional-reduction count. Measured across 20 keys: R\u00b2 = 0.91\u20130.98, \u03b2 = 838\u2013920 ns.\n\n`c` depends on the whole scalar prefix, not on the current bit, so per-step thresholding is useless \u2014 it saturates at ~93% per bit for `u = p\u22121` and at **chance** for `u = 9`, and recovers 0/20 keys either way, because the bit string is a prefix-XOR in which one flipped step inverts the entire tail. Conditioning on the prefix removes the ambiguity: a beam search replays both branches from each candidate ladder state, reads off the exact `c` for each, and scores against the observation. The victim\u0027s public key adjudicates the small residual search.\n\nTwo facts bound the problem and are worth stating precisely, because they determine whether a fix is needed at all:\n\n- **Aggregate observation is provably useless.** The adjacent-bit transition count `T(k)` has exact entropy `H(T) = 4.0357` bits over clamped scalars, so a noiseless transition-count oracle still leaves ~2^247 candidates. The summed reduction count `\u03a3c` is richer (~6.6\u20136.9 bits) and still leaves ~2^244. Any measurement that collapses the call to one number is safe. Per-step measurement is not.\n- **The libgmp call counts are exactly determined.** Per ladder step, `__gmpz_add = 4 + csub`, `__gmpz_sub = 4 + cadd`, `__gmpz_mul = __gmpz_mod = 10`. Verified by differencing gdb breakpoint counts against phpseclib\u0027s own `doubleAndAddPoint` \u2014 27/27 steps exact, extended independently to 64/64 and 38/38 by our two reviewers. An observer that only *counts* these calls needs no timing, no calibration and no repetition.\n\nResults, 20 keys \u00d7 3 sampling seeds, 800 traces per path collected from **800 distinct PHP processes** (so the observations are cross-process, as real requests would be):\n\n| observer | path | observations needed | exact 251-bit recovery |\n|---|---|---|---|\n| timing | key load, `u = 9` | 32 | **20/20 keys**, 95% CI [83.9%, 100%] |\n| timing | ECDH, `u = p\u22121` | 32 | **18/20 keys**, 95% CI [69.9%, 96.8%] |\n| timing | either | 8 | 18\u201323% of trials |\n| libgmp call counts | either | **1** | 20/20 keys; tolerates 20\u201330% of per-step counts being wrong |\n\nThe model underlying the decoder is validated against the pinned implementation: 254/254 (key, peer) outputs match the real `DH::computeSecret`, and all four RFC 7748 \u00a76.1 vectors match both phpseclib and the published constants.\n\nNegative controls are clean \u2014 wrong public key, shuffled trace, wrong peer value, foreign key: 0/20 in every case. Nothing derived from the private key reaches the decoder; its inputs are the observation vector, the peer value, the victim\u0027s public key, and the public clamping constants.\n\n## 5. Impact\n\nIn the instrumented, local model, recovery of the clamped scalar gives a permanent compromise of the X25519 private key. Clamping is applied on every call, so the recovered value is what every past and future operation with that key uses.\n\n## 6. Restrictions\n\n**Required for exploitation:**\n\n1. **A reused / long-lived X25519 private key.** Ephemeral X25519 \u2014 the normal TLS and SSH case \u2014 defeats this outright. phpseclib\u0027s own SSH path generates a fresh scalar per exchange and is not affected.\n2. **Knowledge of the victim\u0027s public key.** It adjudicates the decoder\u0027s residual search; without it no candidate can be selected. This is normally public, but it is a precondition, not a convenience.\n3. **The pure-PHP path must actually run.** Measured across four extension configurations:\n   - `EC::loadFormat(\u0027MontgomeryPrivate\u0027, $raw32)` runs the ladder in **every**\n     configuration \u2014 but the format declares `IS_INVISIBLE` (`MontgomeryPrivate.php:40`),\n     so `PublicKeyLoader::load` skips it and nothing inside phpseclib calls it. An\n     application must name the format explicitly.\n   - `PKCS8` / `PublicKeyLoader::load` / `EC::createKey` run the ladder **only when ext-sodium is absent** \u2014 `PKCS8.php:194` gates on `sodium_crypto_box_publickey_from_secretkey`. OpenSSL does not help here.\n   - `DH::computeSecret` runs the ladder only under `EC::forceEngine(\u0027PHP\u0027)`, or when *both* `openssl_pkey_derive` (`DH.php:325`) and `sodium_crypto_scalarmult` (`EC/PrivateKey.php:75`) are unavailable. ext-sodium is bundled and enabled by default in PHP 7.2+, so the reachable configurations are a minority \u2014 though `disable_functions` hardening and `--disable-sodium` builds do occur, particularly in shared hosting.\n4. **An observer with per-ladder-step resolution**, i.e. one that can distinguish ~0.9 \u00b5s within a ~32 \u00b5s step, or count libgmp entry-point calls. In practice that means local co-residency (e.g. a Flush+Reload spy on the shared `libgmp.so` mapping \u2014 `__gmpz_add` / `__gmpz_sub` are the correct targets; `__gmpn_*` are not, being size-dispatched internals).\n\n**Not demonstrated \u2014 stated so it is not found rather than disclosed:**\n\n- **We did not build a co-resident spy.** Per-step observations in the PoC come from a small `hrtime()` hook inside `Montgomery::multiplyPoint` (one file differs from the pinned tree; `Math/PrimeField/Integer.php`, which carries the leak, is byte-identical). This models an observer with intra-call resolution; it is not itself an attacker capability. We note that the requisite primitives are present on ordinary hardware \u2014 on our test host `clflush` and `rdtscp` work, with  187\u2013312 cycle cached/flushed separation \u2014 but building and validating a spy is separate work we have not done.\n- **Perfect step segmentation is assumed.** A real observer must recover step boundaries and separate the ladder from the surrounding modular inversion.\n- **A single host and configuration.** All timing figures are from one 2-vCPU shared VM (PHP 8.3.6, GMP 6.3.0). A noisier host needs more observations; recovery holds within roughly 25\u201330% additional noise beyond this host\u0027s residual.\n- **We have not identified an affected deployed caller.**\n\nAccordingly we report this as a **hardening issue and demonstrated side channel**, not as a completed remote exploit.\n\n## 8. Suggested remediation\n\n1. **Constant-time, fixed-width field arithmetic, or delegate to a vetted native provider.** This is the actual fix. Removing the ladder\u0027s bit branch is *not* sufficient while `Integer.php:189` and `:207` remain operand-dependent.\n2. **Gate `MontgomeryPrivate.php:66` the way `PKCS8.php:194\u2013200` already is.** That is a one-block change and it closes the only entry point that is un-gated in every configuration. Keep the `$curve instanceof Curve25519` guard \u2014 `MontgomeryPrivate` also accepts Curve448 keys.\n3. **Consider an OpenSSL arm alongside the sodium arm in `PKCS8::loadECDH`**, or fail closed, so stacks without ext-sodium do not fall through to the ladder.\n4. Separately, and unrelated to this channel: the pure-PHP path returns an all-zero 32-byte shared secret for low-order peer inputs. Rejecting the full canonicalised low-order set and adding a constant-time all-zero check is correct hygiene for contributory behaviour \u2014 but it does **not** mitigate the timing channel, since recovery works with `u = 9`.\n\n## Contact\nGeorge Stergiopoulos\nAssistant Professor of cybersecurity\nAthens University of Economics and Business, Greece\nE: geostergiop@aueb.gr | s: https://www.aueb.gr/en/faculty_page/stergiopoulos-georgios",
  "id": "GHSA-q97c-8qh3-fpc6",
  "modified": "2026-09-29T21:15:45Z",
  "published": "2026-09-08T21:24:29Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/phpseclib/phpseclib/security/advisories/GHSA-q97c-8qh3-fpc6"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-84308"
    },
    {
      "type": "WEB",
      "url": "https://github.com/phpseclib/phpseclib/commit/fb56bc5bb9009b54a6c26b31aeec8ed944f17373"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/phpseclib/phpseclib"
    },
    {
      "type": "WEB",
      "url": "https://github.com/phpseclib/phpseclib/releases/tag/3.0.57"
    },
    {
      "type": "WEB",
      "url": "https://github.com/phpseclib/phpseclib/releases/tag/4.0.1"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "phpseclib \u2014 non-constant-time X25519 scalar multiplication permits full private-key recovery"
}

GHSA-QF78-7RGR-8JF5

Vulnerability from github – Published: 2023-01-01 18:30 – Updated: 2023-01-09 18:30
VLAI
Details

A vulnerability classified as problematic was found in Ziftr primecoin up to 0.8.4rc1. Affected by this vulnerability is the function HTTPAuthorized of the file src/bitcoinrpc.cpp. The manipulation of the argument strUserPass/strRPCUserColonPass leads to observable timing discrepancy. Upgrading to version 0.8.4rc2 is able to address this issue. The name of the patch is cdb3441b5cd2c1bae49fae671dc4a496f7c96322. It is recommended to upgrade the affected component. The associated identifier of this vulnerability is VDB-217171.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2013-10006"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-203",
      "CWE-208"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2023-01-01T17:15:00Z",
    "severity": "HIGH"
  },
  "details": "A vulnerability classified as problematic was found in Ziftr primecoin up to 0.8.4rc1. Affected by this vulnerability is the function HTTPAuthorized of the file src/bitcoinrpc.cpp. The manipulation of the argument strUserPass/strRPCUserColonPass leads to observable timing discrepancy. Upgrading to version 0.8.4rc2 is able to address this issue. The name of the patch is cdb3441b5cd2c1bae49fae671dc4a496f7c96322. It is recommended to upgrade the affected component. The associated identifier of this vulnerability is VDB-217171.",
  "id": "GHSA-qf78-7rgr-8jf5",
  "modified": "2023-01-09T18:30:18Z",
  "published": "2023-01-01T18:30:22Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2013-10006"
    },
    {
      "type": "WEB",
      "url": "https://github.com/Ziftr/primecoin/commit/cdb3441b5cd2c1bae49fae671dc4a496f7c96322"
    },
    {
      "type": "WEB",
      "url": "https://github.com/Ziftr/primecoin/releases/tag/v0.8.4rc2"
    },
    {
      "type": "WEB",
      "url": "https://vuldb.com/?ctiid.217171"
    },
    {
      "type": "WEB",
      "url": "https://vuldb.com/?id.217171"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-QJPX-J6J4-8XPR

Vulnerability from github – Published: 2026-05-20 09:30 – Updated: 2026-06-30 03:36
VLAI
Details

In memcached before 1.6.42, username data for SASL password database authentication has a timing side channel because a loop exits as soon as a valid username is found by sasl_server_userdb_checkpass.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2026-47783"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-208"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2026-05-20T07:16:15Z",
    "severity": "HIGH"
  },
  "details": "In memcached before 1.6.42, username data for SASL password database authentication has a timing side channel because a loop exits as soon as a valid username is found by sasl_server_userdb_checkpass.",
  "id": "GHSA-qjpx-j6j4-8xpr",
  "modified": "2026-06-30T03:36:44Z",
  "published": "2026-05-20T09:30:34Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-47783"
    },
    {
      "type": "WEB",
      "url": "https://github.com/memcached/memcached/commit/d13f282b4bce33a9c33b8a1bbf07f12114160fed"
    },
    {
      "type": "WEB",
      "url": "https://access.redhat.com/errata/RHSA-2026:27842"
    },
    {
      "type": "WEB",
      "url": "https://access.redhat.com/errata/RHSA-2026:27862"
    },
    {
      "type": "WEB",
      "url": "https://access.redhat.com/security/cve/CVE-2026-47783"
    },
    {
      "type": "WEB",
      "url": "https://bugzilla.redhat.com/show_bug.cgi?id=2480089"
    },
    {
      "type": "WEB",
      "url": "https://github.com/memcached/memcached/compare/1.6.41...1.6.42"
    },
    {
      "type": "WEB",
      "url": "https://github.com/memcached/memcached/wiki/ReleaseNotes1642"
    },
    {
      "type": "WEB",
      "url": "https://security.access.redhat.com/data/csaf/v2/vex/2026/cve-2026-47783.json"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-QMHP-MCQX-7PPW

Vulnerability from github – Published: 2026-09-09 06:31 – Updated: 2026-09-09 06:31
VLAI
Details

An issue was discovered in the mirage-crypto-ec package before 2.4.0 for OCaml. There is a timing side channel for NIST elliptic-curve scalar multiplication: the time required for a lookup can depend on a secret.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2026-87737"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-208"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2026-09-09T05:18:21Z",
    "severity": "MODERATE"
  },
  "details": "An issue was discovered in the mirage-crypto-ec package before 2.4.0 for OCaml. There is a timing side channel for NIST elliptic-curve scalar multiplication: the time required for a lookup can depend on a secret.",
  "id": "GHSA-qmhp-mcqx-7ppw",
  "modified": "2026-09-09T06:31:39Z",
  "published": "2026-09-09T06:31:39Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-87737"
    },
    {
      "type": "WEB",
      "url": "https://osv.dev/vulnerability/OSEC-2026-17"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-QV5F-57GW-VX3H

Vulnerability from github – Published: 2025-02-10 21:31 – Updated: 2025-03-03 20:10
Withdrawn 2025-03-03 VLAI
Summary
Duplicate Advisory: Authorization Bypass in OPC UA .NET Standard Stack
Details

Duplicate Advisory

This advisory has been withdrawn because it is a duplicate of GHSA-h958-fxgg-g7w3. This link is maintained to preserve external references.

Original Description

Vulnerability in the OPC UA .NET Standard Stack before 1.5.374.158 allows an unauthorized attacker to bypass application authentication when the deprecated Basic128Rsa15 security policy is enabled.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "NuGet",
        "name": "OPCFoundation.NetStandard.Opc.Ua"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "1.5.374.158"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [],
  "database_specific": {
    "cwe_ids": [
      "CWE-208",
      "CWE-639"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2025-02-10T22:57:19Z",
    "nvd_published_at": "2025-02-10T19:15:37Z",
    "severity": "HIGH"
  },
  "details": "## Duplicate Advisory\nThis advisory has been withdrawn because it is a duplicate of GHSA-h958-fxgg-g7w3. This link is maintained to preserve external references.\n\n## Original Description\nVulnerability in the OPC UA .NET Standard Stack before 1.5.374.158 allows an unauthorized attacker to bypass application authentication when the deprecated Basic128Rsa15 security policy is enabled.",
  "id": "GHSA-qv5f-57gw-vx3h",
  "modified": "2025-03-03T20:10:40Z",
  "published": "2025-02-10T21:31:37Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2024-42512"
    },
    {
      "type": "WEB",
      "url": "https://files.opcfoundation.org/SecurityBulletins/OPC%20Foundation%20Security%20Bulletin%20CVE-2024-42512.pdf"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/OPCFoundation/UA-.NETStandard"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:L",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Duplicate Advisory: Authorization Bypass in OPC UA .NET Standard Stack",
  "withdrawn": "2025-03-03T20:10:40Z"
}

GHSA-QWF7-J8G6-3HXV

Vulnerability from github – Published: 2025-08-29 12:31 – Updated: 2025-08-29 12:31
VLAI
Details

Padding oracle attack vulnerability in Oberon microsystem AG’s Oberon PSA Crypto library in all versions since 1.0.0 and prior to 1.5.1 allows an attacker to recover plaintexts via timing measurements of AES-CBC PKCS#7 decrypt operations.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2025-7383"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-208"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2025-08-29T10:15:32Z",
    "severity": "MODERATE"
  },
  "details": "Padding oracle attack vulnerability in Oberon microsystem AG\u2019s Oberon PSA Crypto library in all versions since 1.0.0 and prior to 1.5.1 allows an attacker to recover plaintexts via timing measurements of AES-CBC PKCS#7 decrypt operations.",
  "id": "GHSA-qwf7-j8g6-3hxv",
  "modified": "2025-08-29T12:31:12Z",
  "published": "2025-08-29T12:31:12Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2025-7383"
    },
    {
      "type": "WEB",
      "url": "https://www.oberon.ch/security-advisories/cve-2025-7383"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:4.0/AV:L/AC:H/AT:P/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X",
      "type": "CVSS_V4"
    }
  ]
}

GHSA-R3X7-65M7-5P92

Vulnerability from github – Published: 2026-05-04 15:31 – Updated: 2026-05-04 21:30
VLAI
Details

A timing attack against mod_auth_digest in Apache HTTP Server 2.4.66 allows a bypass of Digest authentication by a remote attacker.

Users are recommended to upgrade to version 2.4.67, which fixes this issue.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2026-33006"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-208"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2026-05-04T15:16:03Z",
    "severity": "MODERATE"
  },
  "details": "A timing attack against mod_auth_digest in Apache HTTP Server 2.4.66 allows a bypass of Digest authentication by a remote attacker.\n\nUsers are recommended to upgrade to version 2.4.67, which fixes this issue.",
  "id": "GHSA-r3x7-65m7-5p92",
  "modified": "2026-05-04T21:30:24Z",
  "published": "2026-05-04T15:31:15Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-33006"
    },
    {
      "type": "WEB",
      "url": "https://httpd.apache.org/security/vulnerabilities_24.html"
    },
    {
      "type": "WEB",
      "url": "http://www.openwall.com/lists/oss-security/2026/05/04/21"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-R4H3-4Q3V-2776

Vulnerability from github – Published: 2026-05-20 09:30 – Updated: 2026-05-20 09:30
VLAI
Details

In memcached before 1.6.42, password data for SASL password database authentication has a timing side channel because memcmp is used by sasl_server_userdb_checkpass.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2026-47784"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-208"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2026-05-20T07:16:15Z",
    "severity": "HIGH"
  },
  "details": "In memcached before 1.6.42, password data for SASL password database authentication has a timing side channel because memcmp is used by sasl_server_userdb_checkpass.",
  "id": "GHSA-r4h3-4q3v-2776",
  "modified": "2026-05-20T09:30:34Z",
  "published": "2026-05-20T09:30:34Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-47784"
    },
    {
      "type": "WEB",
      "url": "https://github.com/memcached/memcached/commit/d13f282b4bce33a9c33b8a1bbf07f12114160fed"
    },
    {
      "type": "WEB",
      "url": "https://github.com/memcached/memcached/compare/1.6.41...1.6.42"
    },
    {
      "type": "WEB",
      "url": "https://github.com/memcached/memcached/wiki/ReleaseNotes1642"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-R854-JRXH-36QX

Vulnerability from github – Published: 2026-04-10 20:58 – Updated: 2026-05-08 15:17
VLAI
Summary
phpseclib has a variable-time HMAC comparison in SSH2::get_binary_packet() using != instead of hash_equals()
Details

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):

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:

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() is constant-time over the same inputs. 3. Applied to 32-byte HMAC-SHA256 values as produced by phpseclib's Crypt\Hash class, the code path at SSH2.php:3405/3410 leaks ~2-14 ns of signal per comparison.

PoC 1: baseline measurement (poc/01_verify_variable_time.php)

Measures != vs hash_equals() on 32-byte strings with first-byte vs last-byte mismatch. 500,000 iterations each, trimmed mean, Welch's t-test.

Representative output (PHP 8.3.6 on Linux x86_64):

=== PHP != operator vs hash_equals() on 32-byte strings ===
Iterations: 500000

!= first-byte mismatch: median=40 p25=40 p75=50 tmean=44.3
!= last-byte mismatch:  median=50 p25=40 p75=50 tmean=46.4
Gap (last - first):     tmean_delta=2.1 ns

hash_equals first-byte: median=110 tmean=112.7
hash_equals last-byte:  median=111 tmean=114.9
Gap (last - first):     tmean_delta=2.2 ns

The 32-byte delta is small (~2 ns) and lives in the noise. This alone doesn't prove variable-time behavior. PoC 2 does.

PoC 2: scaling test (poc/02_scaling_test.php)

The decisive test. If != is truly constant-time, timing should not depend on mismatch position. If it short-circuits, timing should scale linearly with prefix length. Test strings from 32 bytes to 4096 bytes.

Length  32 bytes: range (first-last byte mismatch) =   1.72 ns
Length  64 bytes: range                            =   2.42 ns
Length 128 bytes: range                            =   8.33 ns
Length 256 bytes: range                            =  16.91 ns
Length 512 bytes: range                            =  38.89 ns
Length 1024 bytes: range                           =  81.16 ns
Length 4096 bytes: range                           = 284.26 ns

This is monotone, linear scaling. Confirmed variable-time: != short-circuits. Per-byte delta ≈ 0.089 ns/byte (4096 bytes → ~284 ns → 284/4096 ≈ 0.069 ns/byte after accounting for the fixed call overhead).

PoC 3: contrast with hash_equals() (poc/03_hash_equals_scaling.php)

Same test, != vs hash_equals() on 1024-byte strings:

Mismatch pos         | != tmean (ns)        | hash_equals tmean
----------------------------------------------------------------------
byte    0            |    83.72 (sd  4.79) |   873.78 (sd 100.71)
byte  128            |    98.48 (sd  8.27) |   855.10 (sd 87.50)
byte  256            |   103.36 (sd  7.01) |   869.21 (sd 89.61)
byte  512            |   125.65 (sd 10.07) |   879.74 (sd 81.82)
byte  768            |   145.73 (sd  9.96) |   848.59 (sd 84.34)
byte 1023            |   175.33 (sd 14.31) |   852.24 (sd 85.81)

!= range (last - first):          91.62 ns
hash_equals range (last - first): 31.15 ns

!= monotonically increases with mismatch position. hash_equals is flat; the 31 ns range is measurement jitter (sd ≈ 90 ns > range).

Per-byte delta from this run: 91.62 / 1023 ≈ 0.089 ns/byte. Extrapolated to 32-byte HMAC: ~2.86 ns total signal between first-byte-diff and last-byte-diff.

PoC 4: end-to-end with phpseclib's Hash class (poc/04_phpseclib_in_context.php)

Uses phpseclib4\Crypt\Hash (the exact class bound to $this->hmac_check) to compute a real HMAC-SHA-256, then executes the exact expression $hmac != $this->hmac_check->hash(...) from SSH2.php:3410:

Using != (SSH2.php:3405,3410 pattern):
  first-byte mismatch: 44.33 ns
  last-byte mismatch:  58.19 ns
  Δ (last - first):    13.86 ns  <-- timing leak signal

Using hash_equals() (the fix):
  first-byte mismatch: 113.67 ns
  last-byte mismatch:  112.93 ns
  Δ (last - first):    -0.74 ns  <-- should be flat (noise)

The 13.86 ns vs 2.86 ns variation between runs is within measurement variance — the signal is real and the sign matches every run (last > first for !=, flat for hash_equals).

Impact

Severity: Low (defense-in-depth). This is a real CWE-208 instance, but it is not a practical remote vulnerability.

Why exploitation over the network is infeasible

  1. Signal is tiny. ~3-14 ns per HMAC compare. Network RTT jitter on a reasonable LAN is 100 µs (100,000 ns). On the internet it is 1-10 ms. The signal-to-noise ratio for a remote observer is ~1e-5 to 1e-7.

  2. One measurement per connection. On every MAC failure, SSH2.php:3406/3411 calls disconnect_helper(MAC_ERROR) and throws. The connection is torn down. A reconnect goes through a fresh key exchange, producing a new HMAC key. The HMAC over a fresh key is uncorrelated with the prior HMAC. An attacker cannot accumulate prefix-matching information across connections because there is no fixed target.

  3. MAC is over sequence-numbered data. Each packet's MAC input includes $this->get_seq_no (line 3410) or a nonce (line 3404). Even within a single connection, replays are impossible — every MAC is bound to a distinct seq number. There is no stable oracle to probe.

  4. No adaptive probe. A Bleichenbacher/Lucky13-style attack needs tens of thousands of adaptive queries against a single secret. SSH's hard disconnect eliminates this primitive entirely.

  5. Rough sample-count requirement. To distinguish a 3 ns signal from 1 ms jitter with high confidence you need roughly (noise / signal)^2 ≈ (1e6 / 3)^2 ≈ 1e11 independent samples. With one sample per connection (and each connection being against a fresh secret), this is unreachable on any practical scale.

What remains real

  1. Cryptographic hygiene. A security library should not use non-constant-time comparison on MACs, period. That is standard practice (RFC 4634, NIST SP 800-131A guidance on cryptographic implementations).
  2. Code correctness / API consistency. The library already uses hash_equals() in 9 other comparable places. The SSH2 MAC path is inconsistent with the rest of the codebase.
  3. Future-proofing. If PHP or the underlying memcmp() implementation changes, or if a future MAC algorithm uses longer digests, the signal grows linearly. A constant-time comparison eliminates the concern permanently.
  4. Static analysis / audit hygiene. Cryptographic linters (e.g., GitHub CodeQL's js/timing-attack, phpcs-security-audit) flag non-constant-time comparisons on secret values. A clean codebase passes those checks.

CVSS breakdown

CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N = 3.7 (Low)

  • AV:N — network path, attacker on the SSH transport.
  • AC:H — extremely high complexity; remote exploitation is not demonstrated and is believed infeasible.
  • PR:N/UI:N — no prior auth or interaction needed to observe the timing (if there were any signal).
  • S:U — no scope change.
  • C:L / I:N / A:N — theoretical information exposure of MAC prefix bits only, no integrity or availability impact.

This is the same vector string used for CVE-2026-32935 (the recent AES-CBC padding oracle in the same library), differing only in Confidentiality (L vs H) because padding oracle actually recovers plaintext whereas this MAC timing does not enable any known plaintext recovery.

Fix

Two-line patch:

--- a/phpseclib/Net/SSH2.php
+++ b/phpseclib/Net/SSH2.php
@@ -3402,12 +3402,12 @@ class SSH2
                 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)) {
+                if (!hash_equals($this->hmac_check->hash($reconstructed), $hmac)) {
                     $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))) {
+                if (!hash_equals($this->hmac_check->hash(pack('Na*', $this->get_seq_no, $reconstructed)), $hmac)) {
                     $this->disconnect_helper(DisconnectReason::MAC_ERROR);
                     throw new ConnectionClosedException('Invalid HMAC');
                 }

The same one-liner should be applied to phpseclib/Net/SSH2.php on 3.0 (lines 3741, 3746), 2.0 (line 3796), and 1.0 (line 3810).

hash_equals() is a built-in PHP function available since PHP 5.6. phpseclib's minimum PHP version for all supported branches is well above that, so there is no compatibility concern — and indeed, as noted, hash_equals() is already used throughout the codebase for exactly this purpose.

PoC 5: prefix-position byte-recovery test (poc/05_prefix_position_test.php)

The critical question: can an attacker recover the MAC byte-by-byte? Tests candidate MACs with 0, 1, 2, ..., 31 correct leading bytes using phpseclib's Crypt\Hash class. 500,000 iterations per position.

 0 correct prefix bytes: tmean= 65.75 ns  sd= 4.89 ns
 1 correct prefix bytes: tmean= 66.36 ns  sd= 4.77 ns
 2 correct prefix bytes: tmean= 60.06 ns  sd= 5.29 ns
 4 correct prefix bytes: tmean= 58.08 ns  sd= 5.24 ns
 8 correct prefix bytes: tmean= 66.93 ns  sd= 4.60 ns
12 correct prefix bytes: tmean= 64.01 ns  sd= 4.83 ns
16 correct prefix bytes: tmean= 61.81 ns  sd= 5.55 ns
20 correct prefix bytes: tmean= 60.46 ns  sd= 5.40 ns
24 correct prefix bytes: tmean= 60.53 ns  sd= 4.06 ns
28 correct prefix bytes: tmean= 60.54 ns  sd= 3.99 ns
31 correct prefix bytes: tmean= 59.04 ns  sd= 3.96 ns

Range (0 prefix vs 31 prefix): -6.71 ns (NEGATIVE)

No monotonic signal. The range is negative (wrong direction for an oracle), and the standard deviations (4-5 ns) are larger than any observed position-dependent delta. Byte-by-byte MAC recovery is not feasible even locally on 32-byte HMACs.

This conclusively rules out the "timing oracle for MAC forgery" escalation path.

Escalation angles investigated and ruled out

9 rounds of external review suggested various escalation paths. Every one is dead:

Angle Result Evidence
Byte-by-byte MAC recovery Dead PoC 5: no monotonic signal at 32 bytes (-6.71ns range, wrong sign)
Persistent oracle (connection survives MAC failure) Dead disconnect_helper() + throw is unconditional (3406-3412); reset_connection() clears all state
Sequence desync (Terrapin-style) Dead get_seq_no++ at 3469 is AFTER MAC check; failed MAC throws before increment
SSH_MSG_IGNORE MAC bypass Dead All message types go through get_binary_packet() MAC check before filter()
Memory exhaustion via packet_length Dead packet_length capped at 0x9000 (36KB) at line 3542
State machine abuse / fail-open Dead reset_connection() nulls socket, clears bitmap, all crypto state
Protocol downgrade on MAC failure Dead Immediate disconnect, no fallback or renegotiation
Padding oracle chain (Lucky13) Dead MAC check (3405) is BEFORE padding interpretation (3418)
SFTP subsystem bypass Dead SFTP uses same SSH2 transport, same MAC verification
BigInteger type confusion Dead Engine selected at construction, not per-packet
Pre-auth reachability Yes, but irrelevant One guess per connection with fresh keys = no oracle

Devil's advocate review

Every objection I could think of, addressed:

"This is just a code smell. Show me a real exploit." Acknowledged. The report does not claim remote exploitability. It is submitted as Low / defense-in-depth, consistent with how similar findings are treated in other cryptographic libraries (see e.g. OpenSSH commits replacing memcmp() with timingsafe_bcmp for similar reasons).

"PHP's != might not even short-circuit — you're just seeing noise." PoC 2 rules this out. Timing scales monotonically and nearly linearly with mismatch position across a 128x range of string lengths (32 bytes → 4096 bytes). That is not noise.

"SSH disconnects on MAC failure, so you only get one sample. Case closed." Correct, and the report says so explicitly. This is why the severity is Low rather than High.

"There is no adaptive probe because session keys rotate per connection." Correct, and the report says so explicitly. This is the primary reason the network attack is infeasible.

"Is this Lucky13-style? Does the MAC check happen after padding?" No. MAC verification at SSH2.php:3399-3414 runs BEFORE any padding interpretation. Padding length is only read at line 3418, strictly after the MAC check. There is no unpadding oracle here. (Lucky13 in phpseclib's AES-CBC decrypt path is a separate, already-fixed issue — CVE-2026-32935.)

"Maybe the get_seq_no or nonce path somehow enables forgery." No. The seq number is deterministic (incremented by one per packet) and the nonce (for UMAC) is derived from it. Neither is attacker-controlled in any useful sense. The MAC key is the secret, and it is per-connection.

"Is this already reported?" Searched published advisories (gh api repos/phpseclib/phpseclib/security-advisories). Only CVE-2026-32935 is published, for a different code path (AES-CBC padding oracle after MAC verification). The SSH2 MAC comparison issue is not covered by any published advisory. The fix commit for CVE-2026-32935 (ccc21aef71eb170e9bf819b167e67d1fd9e6e788) did not touch SSH2.php:3405 or :3410.

"Does the project's threat model even consider this in scope?" SECURITY.md says "To report a security vulnerability, please use the Tidelift security contact." It does not exclude timing side-channels. The maintainer has already fixed constant-time comparison issues in other parts of the library (RSA, OAEP, AES-CBC padding) and uses hash_equals() in 9 other places. This issue is consistent with the maintainer's existing security posture, and the fix is trivial.

"Is the Low severity appropriate? Could it be informational?" Low is appropriate. "Informational" would be for something that is provably no-impact. This is a demonstrable CWE-208 instance on secret-dependent data. It scores 3.7 under CVSS v3.1 with AC:H and C:L. That is Low, not Informational. The recent CVE-2026-32935 is assigned for a timing-side-channel in the same file category and given CVSS 4.0 = 8.2 (their vector is more severe because padding oracle actually recovers plaintext). This one would score lower than that, which matches the Low designation.

"Would a maintainer just close this?" Unlikely. The fix is a one-liner, there is no behavioral change, it aligns with existing code style in the same codebase, and it silences future audit findings. The maintainer has a track record of accepting similar hardening patches. If they close it, the reason would likely be "we already know, not worth a CVE" — which would still leave the code fixed, which is the goal.

Suggested reporting path

GitHub PVR is enabled for phpseclib/phpseclib ({"enabled":true}). SECURITY.md says to use the Tidelift contact, but GitHub PVR is a reasonable alternative and lets the maintainer decide whether to coordinate with Tidelift. Either path is acceptable.

Given the Low severity, this could also be filed as a public pull request rather than a security advisory. That would be faster for everyone and avoids using the private disclosure channel for a hardening fix.

Files

  • poc/01_verify_variable_time.php — baseline 32-byte timing measurement
  • poc/02_scaling_test.php — decisive scaling test across string lengths
  • poc/03_hash_equals_scaling.php — != vs hash_equals() comparison
  • poc/04_phpseclib_in_context.php — end-to-end repro using phpseclib's Crypt\Hash
  • notes.md — research notes, dead ends, escalation angles considered
  • prior-work.md — prior work search results
  • status.json — machine-readable status

Koda Reef

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Packagist",
        "name": "phpseclib/phpseclib"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0.1.1"
            },
            {
              "fixed": "1.0.28"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "Packagist",
        "name": "phpseclib/phpseclib"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "2.0.0"
            },
            {
              "fixed": "2.0.53"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "Packagist",
        "name": "phpseclib/phpseclib"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "3.0.0"
            },
            {
              "fixed": "3.0.51"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-40194"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-208"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-04-10T20:58:10Z",
    "nvd_published_at": "2026-04-10T21:16:27Z",
    "severity": "LOW"
  },
  "details": "# phpseclib SSH2: Variable-time comparison in HMAC verification\n\n## Summary\n\n`phpseclib\\Net\\SSH2::get_binary_packet()` uses PHP\u0027s `!=` 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.\n\nThe finding is **Low severity (defense-in-depth)**, not Critical. Practical exploitation over the network is prevented by SSH\u0027s 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.\n\n- **Target:** `phpseclib/phpseclib`\n- **File:** `phpseclib/Net/SSH2.php`\n- **Lines (master `e819a163c`):** 3405 and 3410\n- **CWE:** CWE-208 (Observable Timing Discrepancy)\n- **CVSS v3.1:** 3.7 \u2014 `CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N`\n- **Severity:** Low (cryptographic hygiene / defense-in-depth)\n- **Affected branches:** `master`, `3.0`, `2.0`, `1.0` (all supported versions)\n\n## Root cause\n\n`phpseclib/Net/SSH2.php` lines 3399-3415 (master at `e819a163c`):\n\n```php\nif ($this-\u003ehmac_check instanceof Hash) {\n    $reconstructed = !$this-\u003ehmac_check_etm ?\n        pack(\u0027Na*\u0027, $packet-\u003epacket_length, $packet-\u003eplain) :\n        substr($packet-\u003eraw, 0, -$this-\u003ehmac_size);\n    if (($this-\u003ehmac_check-\u003egetHash() \u0026 \"\\xFF\\xFF\\xFF\\xFF\") == \u0027umac\u0027) {\n        $this-\u003ehmac_check-\u003esetNonce(\"\\0\\0\\0\\0\" . pack(\u0027N\u0027, $this-\u003eget_seq_no));\n        if ($hmac != $this-\u003ehmac_check-\u003ehash($reconstructed)) {                     // \u003c-- line 3405\n            $this-\u003edisconnect_helper(DisconnectReason::MAC_ERROR);\n            throw new ConnectionClosedException(\u0027Invalid UMAC\u0027);\n        }\n    } else {\n        if ($hmac != $this-\u003ehmac_check-\u003ehash(pack(\u0027Na*\u0027, $this-\u003eget_seq_no, $reconstructed))) {  // \u003c-- line 3410\n            $this-\u003edisconnect_helper(DisconnectReason::MAC_ERROR);\n            throw new ConnectionClosedException(\u0027Invalid HMAC\u0027);\n        }\n    }\n}\n```\n\nBoth `$hmac` (read from the socket via `Strings::pop($raw, $this-\u003ehmac_size)` at line 3348) and the computed hash are equal-length binary strings. PHP\u0027s `!=` 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.\n\n### The same bug exists on every supported branch\n\n| Branch | File | Line(s) | Expression |\n|---|---|---|---|\n| `master` (@ `e819a163c`) | `phpseclib/Net/SSH2.php` | 3405, 3410 | `$hmac != $this-\u003ehmac_check-\u003ehash(...)` |\n| `3.0` | `phpseclib/Net/SSH2.php` | 3741, 3746 | `$hmac != $this-\u003ehmac_check-\u003ehash(...)` |\n| `2.0` | `phpseclib/Net/SSH2.php` | 3796 | `$hmac != $this-\u003ehmac_check-\u003ehash(...)` |\n| `1.0` | `phpseclib/Net/SSH2.php` | 3810 | `$hmac != $this-\u003ehmac_check-\u003ehash(...)` |\n\nVerified with `git show \u003cbranch\u003e:phpseclib/Net/SSH2.php`.\n\n### Reachability\n\nThe HMAC verification path at lines 3399-3415 is reached on **every received SSH packet** when the negotiated cipher is not AEAD \u2014 i.e., any of:\n\n- `aes128-cbc`, `aes192-cbc`, `aes256-cbc`\n- `aes128-ctr`, `aes192-ctr`, `aes256-ctr`\n- `3des-cbc`, `3des-ctr`\n- `blowfish-cbc`, `blowfish-ctr`\n- `twofish-*-cbc`, `twofish-*-ctr`\n- `arcfour`, `arcfour128`, `arcfour256`\n\ncombined 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 \u2014 full list at `getSupportedMACAlgorithms()` around line 4754).\n\nAEAD 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 \u2014 their authentication tag is checked inside the AEAD implementation.\n\n`$this-\u003ehmac_check` is set during key exchange at `SSH2.php:1812`:\n\n```php\nif (!$this-\u003edecrypt-\u003eusesNonce()) {\n    [$this-\u003ehmac_check, $checkKeyLength] = self::mac_algorithm_to_hash_instance($mac_algorithm_in);\n    $this-\u003ehmac_size = $this-\u003ehmac_check-\u003egetLengthInBytes();\n}\n```\n\nSo any SSH2 client session negotiating a non-AEAD cipher exercises the vulnerable code path starting from the first post-KEX packet.\n\n### Contrast with existing code that does the right thing\n\n`hash_equals()` is already used in 9 other places in the phpseclib codebase, proving the maintainer knows the pattern:\n\n```\nphpseclib/File/CMS/DigestedData.php:186\nphpseclib/File/CMS/SignedData/Signer.php:294\nphpseclib/File/CMS/SignedData/Signer.php:406\nphpseclib/Crypt/Common/Formats/Keys/PuTTY.php:236\nphpseclib/Crypt/RSA/PublicKey.php:98, 105, 188, 229\nphpseclib/Crypt/RSA/PrivateKey.php:386\n```\n\nThe SSH2 MAC comparison is the one place it was missed.\n\n## Proof of concept\n\nAll scripts under `poc/`. They prove three things:\n1. PHP\u0027s `!=` on equal-length binary strings IS variable-time and short-circuits.\n2. `hash_equals()` is constant-time over the same inputs.\n3. Applied to 32-byte HMAC-SHA256 values as produced by phpseclib\u0027s `Crypt\\Hash` class, the code path at `SSH2.php:3405/3410` leaks ~2-14 ns of signal per comparison.\n\n### PoC 1: baseline measurement (`poc/01_verify_variable_time.php`)\n\nMeasures `!=` vs `hash_equals()` on 32-byte strings with first-byte vs last-byte mismatch. 500,000 iterations each, trimmed mean, Welch\u0027s t-test.\n\nRepresentative output (PHP 8.3.6 on Linux x86_64):\n\n```\n=== PHP != operator vs hash_equals() on 32-byte strings ===\nIterations: 500000\n\n!= first-byte mismatch: median=40 p25=40 p75=50 tmean=44.3\n!= last-byte mismatch:  median=50 p25=40 p75=50 tmean=46.4\nGap (last - first):     tmean_delta=2.1 ns\n\nhash_equals first-byte: median=110 tmean=112.7\nhash_equals last-byte:  median=111 tmean=114.9\nGap (last - first):     tmean_delta=2.2 ns\n```\n\nThe 32-byte delta is small (~2 ns) and lives in the noise. This alone doesn\u0027t prove variable-time behavior. PoC 2 does.\n\n### PoC 2: scaling test (`poc/02_scaling_test.php`)\n\nThe decisive test. If `!=` is truly constant-time, timing should not depend on mismatch position. If it short-circuits, timing should scale linearly with prefix length. Test strings from 32 bytes to 4096 bytes.\n\n```\nLength  32 bytes: range (first-last byte mismatch) =   1.72 ns\nLength  64 bytes: range                            =   2.42 ns\nLength 128 bytes: range                            =   8.33 ns\nLength 256 bytes: range                            =  16.91 ns\nLength 512 bytes: range                            =  38.89 ns\nLength 1024 bytes: range                           =  81.16 ns\nLength 4096 bytes: range                           = 284.26 ns\n```\n\n**This is monotone, linear scaling.** Confirmed variable-time: `!=` short-circuits. Per-byte delta \u2248 0.089 ns/byte (4096 bytes \u2192 ~284 ns \u2192 284/4096 \u2248 0.069 ns/byte after accounting for the fixed call overhead).\n\n### PoC 3: contrast with `hash_equals()` (`poc/03_hash_equals_scaling.php`)\n\nSame test, `!=` vs `hash_equals()` on 1024-byte strings:\n\n```\nMismatch pos         | != tmean (ns)        | hash_equals tmean\n----------------------------------------------------------------------\nbyte    0            |    83.72 (sd  4.79) |   873.78 (sd 100.71)\nbyte  128            |    98.48 (sd  8.27) |   855.10 (sd 87.50)\nbyte  256            |   103.36 (sd  7.01) |   869.21 (sd 89.61)\nbyte  512            |   125.65 (sd 10.07) |   879.74 (sd 81.82)\nbyte  768            |   145.73 (sd  9.96) |   848.59 (sd 84.34)\nbyte 1023            |   175.33 (sd 14.31) |   852.24 (sd 85.81)\n\n!= range (last - first):          91.62 ns\nhash_equals range (last - first): 31.15 ns\n```\n\n`!=` monotonically increases with mismatch position. `hash_equals` is flat; the 31 ns range is measurement jitter (sd \u2248 90 ns \u003e range).\n\nPer-byte delta from this run: 91.62 / 1023 \u2248 0.089 ns/byte. Extrapolated to 32-byte HMAC: ~2.86 ns total signal between first-byte-diff and last-byte-diff.\n\n### PoC 4: end-to-end with phpseclib\u0027s Hash class (`poc/04_phpseclib_in_context.php`)\n\nUses `phpseclib4\\Crypt\\Hash` (the exact class bound to `$this-\u003ehmac_check`) to compute a real HMAC-SHA-256, then executes the exact expression `$hmac != $this-\u003ehmac_check-\u003ehash(...)` from SSH2.php:3410:\n\n```\nUsing != (SSH2.php:3405,3410 pattern):\n  first-byte mismatch: 44.33 ns\n  last-byte mismatch:  58.19 ns\n  \u0394 (last - first):    13.86 ns  \u003c-- timing leak signal\n\nUsing hash_equals() (the fix):\n  first-byte mismatch: 113.67 ns\n  last-byte mismatch:  112.93 ns\n  \u0394 (last - first):    -0.74 ns  \u003c-- should be flat (noise)\n```\n\nThe 13.86 ns vs 2.86 ns variation between runs is within measurement variance \u2014 the signal is real and the sign matches every run (last \u003e first for `!=`, flat for `hash_equals`).\n\n## Impact\n\n**Severity: Low (defense-in-depth).** This is a real CWE-208 instance, but it is not a practical remote vulnerability.\n\n### Why exploitation over the network is infeasible\n\n1. **Signal is tiny.** ~3-14 ns per HMAC compare. Network RTT jitter on a reasonable LAN is 100 \u00b5s (100,000 ns). On the internet it is 1-10 ms. The signal-to-noise ratio for a remote observer is ~1e-5 to 1e-7.\n\n2. **One measurement per connection.** On every MAC failure, `SSH2.php:3406/3411` calls `disconnect_helper(MAC_ERROR)` and throws. The connection is torn down. A reconnect goes through a fresh key exchange, producing a new HMAC key. The HMAC over a fresh key is uncorrelated with the prior HMAC. An attacker cannot accumulate prefix-matching information across connections because there is no fixed target.\n\n3. **MAC is over sequence-numbered data.** Each packet\u0027s MAC input includes `$this-\u003eget_seq_no` (line 3410) or a nonce (line 3404). Even within a single connection, replays are impossible \u2014 every MAC is bound to a distinct seq number. There is no stable oracle to probe.\n\n4. **No adaptive probe.** A Bleichenbacher/Lucky13-style attack needs tens of thousands of adaptive queries against a single secret. SSH\u0027s hard disconnect eliminates this primitive entirely.\n\n5. **Rough sample-count requirement.** To distinguish a 3 ns signal from 1 ms jitter with high confidence you need roughly `(noise / signal)^2 \u2248 (1e6 / 3)^2 \u2248 1e11` independent samples. With one sample per connection (and each connection being against a fresh secret), this is unreachable on any practical scale.\n\n### What remains real\n\n1. **Cryptographic hygiene.** A security library should not use non-constant-time comparison on MACs, period. That is standard practice (RFC 4634, NIST SP 800-131A guidance on cryptographic implementations).\n2. **Code correctness / API consistency.** The library already uses `hash_equals()` in 9 other comparable places. The SSH2 MAC path is inconsistent with the rest of the codebase.\n3. **Future-proofing.** If PHP or the underlying `memcmp()` implementation changes, or if a future MAC algorithm uses longer digests, the signal grows linearly. A constant-time comparison eliminates the concern permanently.\n4. **Static analysis / audit hygiene.** Cryptographic linters (e.g., GitHub CodeQL\u0027s `js/timing-attack`, phpcs-security-audit) flag non-constant-time comparisons on secret values. A clean codebase passes those checks.\n\n### CVSS breakdown\n\n`CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N` = **3.7 (Low)**\n\n- **AV:N** \u2014 network path, attacker on the SSH transport.\n- **AC:H** \u2014 extremely high complexity; remote exploitation is not demonstrated and is believed infeasible.\n- **PR:N/UI:N** \u2014 no prior auth or interaction needed to observe the timing (if there were any signal).\n- **S:U** \u2014 no scope change.\n- **C:L / I:N / A:N** \u2014 theoretical information exposure of MAC prefix bits only, no integrity or availability impact.\n\nThis is the same vector string used for CVE-2026-32935 (the recent AES-CBC padding oracle in the same library), differing only in Confidentiality (L vs H) because padding oracle actually recovers plaintext whereas this MAC timing does not enable any known plaintext recovery.\n\n## Fix\n\nTwo-line patch:\n\n```diff\n--- a/phpseclib/Net/SSH2.php\n+++ b/phpseclib/Net/SSH2.php\n@@ -3402,12 +3402,12 @@ class SSH2\n                 substr($packet-\u003eraw, 0, -$this-\u003ehmac_size);\n             if (($this-\u003ehmac_check-\u003egetHash() \u0026 \"\\xFF\\xFF\\xFF\\xFF\") == \u0027umac\u0027) {\n                 $this-\u003ehmac_check-\u003esetNonce(\"\\0\\0\\0\\0\" . pack(\u0027N\u0027, $this-\u003eget_seq_no));\n-                if ($hmac != $this-\u003ehmac_check-\u003ehash($reconstructed)) {\n+                if (!hash_equals($this-\u003ehmac_check-\u003ehash($reconstructed), $hmac)) {\n                     $this-\u003edisconnect_helper(DisconnectReason::MAC_ERROR);\n                     throw new ConnectionClosedException(\u0027Invalid UMAC\u0027);\n                 }\n             } else {\n-                if ($hmac != $this-\u003ehmac_check-\u003ehash(pack(\u0027Na*\u0027, $this-\u003eget_seq_no, $reconstructed))) {\n+                if (!hash_equals($this-\u003ehmac_check-\u003ehash(pack(\u0027Na*\u0027, $this-\u003eget_seq_no, $reconstructed)), $hmac)) {\n                     $this-\u003edisconnect_helper(DisconnectReason::MAC_ERROR);\n                     throw new ConnectionClosedException(\u0027Invalid HMAC\u0027);\n                 }\n```\n\nThe same one-liner should be applied to `phpseclib/Net/SSH2.php` on `3.0` (lines 3741, 3746), `2.0` (line 3796), and `1.0` (line 3810).\n\n`hash_equals()` is a built-in PHP function available since PHP 5.6. phpseclib\u0027s minimum PHP version for all supported branches is well above that, so there is no compatibility concern \u2014 and indeed, as noted, `hash_equals()` is already used throughout the codebase for exactly this purpose.\n\n### PoC 5: prefix-position byte-recovery test (`poc/05_prefix_position_test.php`)\n\nThe critical question: can an attacker recover the MAC byte-by-byte? Tests candidate MACs with 0, 1, 2, ..., 31 correct leading bytes using phpseclib\u0027s `Crypt\\Hash` class. 500,000 iterations per position.\n\n```\n 0 correct prefix bytes: tmean= 65.75 ns  sd= 4.89 ns\n 1 correct prefix bytes: tmean= 66.36 ns  sd= 4.77 ns\n 2 correct prefix bytes: tmean= 60.06 ns  sd= 5.29 ns\n 4 correct prefix bytes: tmean= 58.08 ns  sd= 5.24 ns\n 8 correct prefix bytes: tmean= 66.93 ns  sd= 4.60 ns\n12 correct prefix bytes: tmean= 64.01 ns  sd= 4.83 ns\n16 correct prefix bytes: tmean= 61.81 ns  sd= 5.55 ns\n20 correct prefix bytes: tmean= 60.46 ns  sd= 5.40 ns\n24 correct prefix bytes: tmean= 60.53 ns  sd= 4.06 ns\n28 correct prefix bytes: tmean= 60.54 ns  sd= 3.99 ns\n31 correct prefix bytes: tmean= 59.04 ns  sd= 3.96 ns\n\nRange (0 prefix vs 31 prefix): -6.71 ns (NEGATIVE)\n```\n\n**No monotonic signal.** The range is negative (wrong direction for an oracle), and the standard deviations (4-5 ns) are larger than any observed position-dependent delta. Byte-by-byte MAC recovery is not feasible even locally on 32-byte HMACs.\n\nThis conclusively rules out the \"timing oracle for MAC forgery\" escalation path.\n\n## Escalation angles investigated and ruled out\n\n9 rounds of external review suggested various escalation paths. Every one is dead:\n\n| Angle | Result | Evidence |\n|---|---|---|\n| Byte-by-byte MAC recovery | **Dead** | PoC 5: no monotonic signal at 32 bytes (-6.71ns range, wrong sign) |\n| Persistent oracle (connection survives MAC failure) | **Dead** | disconnect_helper() + throw is unconditional (3406-3412); reset_connection() clears all state |\n| Sequence desync (Terrapin-style) | **Dead** | get_seq_no++ at 3469 is AFTER MAC check; failed MAC throws before increment |\n| SSH_MSG_IGNORE MAC bypass | **Dead** | All message types go through get_binary_packet() MAC check before filter() |\n| Memory exhaustion via packet_length | **Dead** | packet_length capped at 0x9000 (36KB) at line 3542 |\n| State machine abuse / fail-open | **Dead** | reset_connection() nulls socket, clears bitmap, all crypto state |\n| Protocol downgrade on MAC failure | **Dead** | Immediate disconnect, no fallback or renegotiation |\n| Padding oracle chain (Lucky13) | **Dead** | MAC check (3405) is BEFORE padding interpretation (3418) |\n| SFTP subsystem bypass | **Dead** | SFTP uses same SSH2 transport, same MAC verification |\n| BigInteger type confusion | **Dead** | Engine selected at construction, not per-packet |\n| Pre-auth reachability | Yes, but irrelevant | One guess per connection with fresh keys = no oracle |\n\n## Devil\u0027s advocate review\n\nEvery objection I could think of, addressed:\n\n**\"This is just a code smell. Show me a real exploit.\"**\nAcknowledged. The report does not claim remote exploitability. It is submitted as Low / defense-in-depth, consistent with how similar findings are treated in other cryptographic libraries (see e.g. OpenSSH commits replacing `memcmp()` with `timingsafe_bcmp` for similar reasons).\n\n**\"PHP\u0027s `!=` might not even short-circuit \u2014 you\u0027re just seeing noise.\"**\nPoC 2 rules this out. Timing scales monotonically and nearly linearly with mismatch position across a 128x range of string lengths (32 bytes \u2192 4096 bytes). That is not noise.\n\n**\"SSH disconnects on MAC failure, so you only get one sample. Case closed.\"**\nCorrect, and the report says so explicitly. This is why the severity is Low rather than High.\n\n**\"There is no adaptive probe because session keys rotate per connection.\"**\nCorrect, and the report says so explicitly. This is the primary reason the network attack is infeasible.\n\n**\"Is this Lucky13-style? Does the MAC check happen after padding?\"**\nNo. MAC verification at `SSH2.php:3399-3414` runs BEFORE any padding interpretation. Padding length is only read at line 3418, strictly after the MAC check. There is no unpadding oracle here. (Lucky13 in phpseclib\u0027s AES-CBC decrypt path is a separate, already-fixed issue \u2014 CVE-2026-32935.)\n\n**\"Maybe the `get_seq_no` or nonce path somehow enables forgery.\"**\nNo. The seq number is deterministic (incremented by one per packet) and the nonce (for UMAC) is derived from it. Neither is attacker-controlled in any useful sense. The MAC key is the secret, and it is per-connection.\n\n**\"Is this already reported?\"**\nSearched published advisories (`gh api repos/phpseclib/phpseclib/security-advisories`). Only CVE-2026-32935 is published, for a different code path (AES-CBC padding oracle after MAC verification). The SSH2 MAC comparison issue is not covered by any published advisory. The fix commit for CVE-2026-32935 (`ccc21aef71eb170e9bf819b167e67d1fd9e6e788`) did not touch `SSH2.php:3405` or `:3410`.\n\n**\"Does the project\u0027s threat model even consider this in scope?\"**\nSECURITY.md says \"To report a security vulnerability, please use the Tidelift security contact.\" It does not exclude timing side-channels. The maintainer has already fixed constant-time comparison issues in other parts of the library (RSA, OAEP, AES-CBC padding) and uses `hash_equals()` in 9 other places. This issue is consistent with the maintainer\u0027s existing security posture, and the fix is trivial.\n\n**\"Is the Low severity appropriate? Could it be informational?\"**\nLow is appropriate. \"Informational\" would be for something that is provably no-impact. This is a demonstrable CWE-208 instance on secret-dependent data. It scores 3.7 under CVSS v3.1 with AC:H and C:L. That is Low, not Informational. The recent CVE-2026-32935 is assigned for a timing-side-channel in the same file category and given CVSS 4.0 = 8.2 (their vector is more severe because padding oracle actually recovers plaintext). This one would score lower than that, which matches the Low designation.\n\n**\"Would a maintainer just close this?\"**\nUnlikely. The fix is a one-liner, there is no behavioral change, it aligns with existing code style in the same codebase, and it silences future audit findings. The maintainer has a track record of accepting similar hardening patches. If they close it, the reason would likely be \"we already know, not worth a CVE\" \u2014 which would still leave the code fixed, which is the goal.\n\n## Suggested reporting path\n\nGitHub PVR is enabled for phpseclib/phpseclib (`{\"enabled\":true}`). SECURITY.md says to use the Tidelift contact, but GitHub PVR is a reasonable alternative and lets the maintainer decide whether to coordinate with Tidelift. Either path is acceptable.\n\nGiven the Low severity, this could also be filed as a public pull request rather than a security advisory. That would be faster for everyone and avoids using the private disclosure channel for a hardening fix.\n\n## Files\n\n- `poc/01_verify_variable_time.php` \u2014 baseline 32-byte timing measurement\n- `poc/02_scaling_test.php` \u2014 decisive scaling test across string lengths\n- `poc/03_hash_equals_scaling.php` \u2014 `!=` vs `hash_equals()` comparison\n- `poc/04_phpseclib_in_context.php` \u2014 end-to-end repro using phpseclib\u0027s `Crypt\\Hash`\n- `notes.md` \u2014 research notes, dead ends, escalation angles considered\n- `prior-work.md` \u2014 prior work search results\n- `status.json` \u2014 machine-readable status\n\n\nKoda Reef",
  "id": "GHSA-r854-jrxh-36qx",
  "modified": "2026-05-08T15:17:47Z",
  "published": "2026-04-10T20:58:10Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/phpseclib/phpseclib/security/advisories/GHSA-r854-jrxh-36qx"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-40194"
    },
    {
      "type": "WEB",
      "url": "https://github.com/phpseclib/phpseclib/commit/ffe48b6b1b1af6963327f0a5330e3aa004a194ac"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/phpseclib/phpseclib"
    },
    {
      "type": "WEB",
      "url": "https://github.com/phpseclib/phpseclib/releases/tag/1.0.28"
    },
    {
      "type": "WEB",
      "url": "https://github.com/phpseclib/phpseclib/releases/tag/2.0.53"
    },
    {
      "type": "WEB",
      "url": "https://github.com/phpseclib/phpseclib/releases/tag/3.0.51"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "phpseclib has a variable-time HMAC comparison in SSH2::get_binary_packet() using != instead of hash_equals()"
}

GHSA-R894-5R7V-7RX3

Vulnerability from github – Published: 2023-01-07 09:30 – Updated: 2023-01-12 23:40
VLAI
Summary
easy-scrypt Observable Timing Discrepancy vulnerability
Details

A vulnerability, which was classified as problematic, was found in agnivade easy-scrypt. Affected is the function VerifyPassphrase of the file scrypt.go. The manipulation leads to observable timing discrepancy. Upgrading to version 1.0.0 can address this issue. The name of the patch is 477c10cf3b144ddf96526aa09f5fdea613f21812. It is recommended to upgrade the affected component. The identifier of this vulnerability is VDB-217596.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Go",
        "name": "github.com/agnivade/easy-scrypt"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "1.0.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2014-125055"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-208"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2023-01-12T23:40:22Z",
    "nvd_published_at": "2023-01-07T09:15:00Z",
    "severity": "MODERATE"
  },
  "details": "A vulnerability, which was classified as problematic, was found in agnivade easy-scrypt. Affected is the function `VerifyPassphrase` of the file `scrypt.go`. The manipulation leads to observable timing discrepancy. Upgrading to version 1.0.0 can address this issue. The name of the patch is 477c10cf3b144ddf96526aa09f5fdea613f21812. It is recommended to upgrade the affected component. The identifier of this vulnerability is VDB-217596.",
  "id": "GHSA-r894-5r7v-7rx3",
  "modified": "2023-01-12T23:40:22Z",
  "published": "2023-01-07T09:30:17Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2014-125055"
    },
    {
      "type": "WEB",
      "url": "https://github.com/agnivade/easy-scrypt/commit/477c10cf3b144ddf96526aa09f5fdea613f21812"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/agnivade/easy-scrypt"
    },
    {
      "type": "WEB",
      "url": "https://github.com/agnivade/easy-scrypt/releases/tag/v1.0.0"
    },
    {
      "type": "WEB",
      "url": "https://vuldb.com/?ctiid.217596"
    },
    {
      "type": "WEB",
      "url": "https://vuldb.com/?id.217596"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "easy-scrypt Observable Timing Discrepancy vulnerability"
}

No mitigation information available for this CWE.

CAPEC-462: Cross-Domain Search Timing

An attacker initiates cross domain HTTP / GET requests and times the server responses. The timing of these responses may leak important information on what is happening on the server. Browser's same origin policy prevents the attacker from directly reading the server responses (in the absence of any other weaknesses), but does not prevent the attacker from timing the responses to requests that the attacker issued cross domain.

CAPEC-541: Application Fingerprinting

An adversary engages in fingerprinting activities to determine the type or version of an application installed on a remote target.

CAPEC-580: System Footprinting

An adversary engages in active probing and exploration activities to determine security information about a remote target system. Often times adversaries will rely on remote applications that can be probed for system configurations.