Phalcon: Non-constant-time HMAC verification in `Encryption\Crypt::decrypt` (timing side-channel)GHSA-8jqh-95g6-7jpj
Fix: phalcon/cphalcon#17091GHSA-8jqh-95g6-7jpj is a CWE-208 vulnerability in phalcon/cphalcon. A fix is available for phalcon/cphalcon — see the affected versions and patch details below.
Exploitation Status
No confirmed exploitation observed yet
- CISA’s own triage has not observed active exploitation or public proof-of-concept code for this CVE as of its last assessment.
Exploitation and automatability from CISA’s SSVC triage for GHSA-8jqh-95g6-7jpj.
EPSS Exploitation Probability
Probability of exploitation in the next 30 days, from FIRST.org EPSS.
Real-World Exposure
phalcon/cphalconReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects Packagist packages — download data is not available via public APIs for these ecosystems.
Description
Summary
Phalcon\Encryption\Crypt provides authenticated encryption: when useSigning is enabled (the default), encrypt() appends an HMAC tag and decrypt() verifies it before returning the plaintext. The verification compares the attacker-supplied tag against the freshly computed HMAC using PHP/Zephir identity comparison (!==), which the Zephir compiler lowers to !ZEPHIR_IS_IDENTICAL(...) — a byte-wise memcmp that returns early on the first differing byte. The comparison time therefore depends on how many leading bytes of the supplied tag are correct, a classic MAC-verification timing side-channel. Every other secret/MAC comparison in the framework uses the constant-time hash_equals() (zephir_hash_equals) — the CSRF token check (Security::checkToken) and the JWT signature check (Signer\Hmac::verify); Crypt::decrypt is the lone deviation.
Details
Vulnerable code
phalcon/Encryption/Crypt.zep:246 (Zephir source):
if true === this->useSigning {
// Checks on the decrypted message digest using the HMAC method.
if digest !== hash_hmac(hashAlgorithm, padded, decryptKey, true) {
throw new Mismatch("Hash does not match.");
}
}
Generated C --> ext/phalcon/encryption/crypt.zep.c:364-367:
ZEPHIR_CALL_FUNCTION(&_8$$7, "hash_hmac", NULL, 245, &hashAlgorithm, &padded, &decryptKey, &__$true);
...
if (!ZEPHIR_IS_IDENTICAL(&digest, &_8$$7)) { // <-- non-constant-time
ZEPHIR_THROW_EXCEPTION_DEBUG_STR(..., "Hash does not match.", "phalcon/Encryption/Crypt.zep", 247);
ZEPHIR_IS_IDENTICAL --> zephir_is_identical() (ext/kernel/operators.c:472) --> Zend is_identical_function --> for equal-length strings a memcmp that exits on the first mismatching byte (data-dependent timing).
Impact
The HMAC is the integrity/authentication tag of Phalcon's authenticated-encryption scheme. A successful timing attack (Keyczar/CVE-2009-0654-style: fix the IV+ciphertext so the target tag is constant, then recover it byte-by-byte from response timing) yields a tag the attacker can attach to a chosen IV+ciphertext so that decrypt() accepts it as authentic, defeating the integrity guarantee. Combined with CFB malleability (flipping a ciphertext byte flips the corresponding plaintext byte), an attacker who recovers the forging capability can tamper with the decrypted contents the application trusts (e.g. encrypted cookies carrying authorization/identity state). There is no confidentiality break by itself.
Suggested fix
Replace the identity comparison with the constant-time helper already used elsewhere in the framework. In phalcon/Encryption/Crypt.zep:246:
// before
if digest !== hash_hmac(hashAlgorithm, padded, decryptKey, true) {
throw new Mismatch("Hash does not match.");
}
// after
if true !== hash_equals(hash_hmac(hashAlgorithm, padded, decryptKey, true), digest) {
throw new Mismatch("Hash does not match.");
}
hash_equals() returns false for unequal-length inputs, so it also covers the truncated-tag case. Optional further hardening: verify the MAC before unpadding (functionally moot here because cryptUnpadText never throws) and consider migrating the default toward an AEAD mode such as aes-256-gcm.
Addressed Issue:
Patched Stream:
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐘Packagist | phalcon/cphalcon | all versions | 5.14.1composer require phalcon/cphalcon:^5.14.1 |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for phalcon/cphalcon, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
Fix
Update phalcon/cphalcon to 5.14.1 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-8jqh-95g6-7jpj is resolved across your whole dependency graph.
Workarounds
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
Frequently Asked Questions
Is GHSA-8jqh-95g6-7jpj in your dependencies?
Find it across Packagist, including transitive dependencies.