PT-2026-53185 · Packagist · Web-Token/Jwt-Framework+1
Published
2026-06-18
·
Updated
2026-06-18
CVSS v4.0
8.1
High
| Vector | AV:N/AC:H/AT:N/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N/E:P |
Summary
JWSVerifier::getAlgorithm() in src/Library/Signature/JWSVerifier.php (line 144) merges protected and unprotected headers using PHP's spread operator:php
$completeHeader = [...$signature->getProtectedHeader(), ...$signature->getHeader()];In PHP, when spreading arrays with duplicate string keys, the last array's values take precedence. Since the unprotected header (
getHeader()) is spread second, an attacker can override the integrity-protected alg parameter by placing a different value in the unprotected header.This creates a Time-of-Check/Time-of-Use (TOCTOU) vulnerability:
HeaderCheckerManagervalidatesalgfrom the protected headerJWSVerifierusesalgfrom the unprotected header for actual verification
The same issue exists in
JWEDecrypter.php (lines 120-124) where array merge() exhibits the same last-wins behavior for alg and enc.Affected Code
JWSVerifier.php line 144 — Spread operator merge order allows unprotected header to override
alg:php
$completeHeader = [...$signature->getProtectedHeader(), ...$signature->getHeader()];JWEDecrypter.php lines 120-124 —
array merge() with same last-wins behavior:php
$completeHeader = array merge(
$jwe->getSharedProtectedHeader(),
$jwe->getSharedHeader(),
$recipient->getHeader()
);Attack Vectors
Vector A — Mixed key sets (HIGH probability)
If the application uses a JWKSet containing keys of different types (common in multi-tenant or federation scenarios), the JWSVerifier iterates all keys (line 86). An attacker can force a different algorithm that matches a different key in the set.
Vector B — alg ONLY in unprotected header (HIGH probability)
If
alg is placed EXCLUSIVELY in the unprotected header (not in the protected header at all), HeaderCheckerManager::checkDuplicatedHeaderParameters() does NOT trigger. The JSON Flattened/General serializers allow tokens with no protected header or a protected header without alg. RFC 7515 Section 4.1.1 states alg MUST be integrity-protected, but the library does not enforce this.Vector C — Direct JWSVerifier usage (HIGH probability)
JWSLoader takes ?HeaderCheckerManager (nullable). If developers use JWSVerifier directly or create JWSLoader without a HeaderCheckerManager, the duplicate header check never runs.Contrast with JWSBuilder (safe)
JWSBuilder::findSignatureAlgorithm() (line 196) uses [...$header, ...$protectedHeader] where protected wins. It also has checkDuplicatedHeaderParameters() (line 218). The JWSVerifier has neither safeguard.Proof of Concept
php
<?php
// Demonstrate algorithm override via unprotected header
$protected = ["alg" => "RS256", "typ" => "JWT"];
$unprotected = ["alg" => "HS256"];
$merged = [...$protected, ...$unprotected];
// $merged["alg"] === "HS256" — unprotected wins!
// JSON Flattened JWS with algorithm override:
$maliciousJws = json encode([
'payload' => base64url encode($payload),
'protected' => base64url encode('{"alg":"RS256"}'),
'header' => ['alg' => 'HS256'], // OVERRIDE
'signature' => base64url encode($sig),
]);
// HeaderCheckerManager validates RS256 from protected header -> PASS
// JWSVerifier uses HS256 from unprotected header -> attacker's algorithm choiceA full working PoC demonstrating HS512-to-HS256 downgrade with mixed keysets is available upon request.
Suggested Fix
In
JWSVerifier::getAlgorithm(), read alg exclusively from the protected header:php
private function getAlgorithm(Signature $signature): Algorithm
{
$protectedHeader = $signature->getProtectedHeader();
if (! isset($protectedHeader['alg'])) {
throw new InvalidArgumentException('The "alg" parameter must be in the protected header.');
}
return $this->signatureAlgorithmManager->get($protectedHeader['alg']);
}For
JWEDecrypter, reverse the merge order so protected header wins, or extract alg/enc exclusively from the protected header.Résolution
Un correctif a été préparé sur une branche dédiée basée sur
3.4.x, avec des tests anti-régression dédiés (fork privé temporaire de cette advisory, PR #1).JWS algorithm confusion —
JWSVerifier lit le paramètre alg exclusivement dans le header protégé en intégrité (RFC 7515 §4.1.1) ; un alg placé dans le header non protégé ne peut plus surcharger l'algorithme signé.Validation :
php -l OK, PHPUnit vert, aucune nouvelle erreur PHPStan introduite (différentiel nul vs 3.4.x), aucun commentaire ajouté dans le code source. Après merge, cascade prévue 3.4.x → 4.0.x → 4.1.x.Fix
Insufficient Verification of Data Authenticity
Found an issue in the description? Have something to add? Feel free to write us 👾
Weakness Enumeration
Related Identifiers
Affected Products
Web-Token/Jwt-Framework
Web-Token/Jwt-Library