{"id":"GHSA-jc38-x7x8-2xc8","aliases":[],"title":"PHP JWT Framework: JWSVerifier uses algorithm from unprotected header, enabling algorithm confusion attacks","description":"## Summary\n\n`JWSVerifier::getAlgorithm()` in `src/Library/Signature/JWSVerifier.php` (line 144) merges protected and unprotected headers using PHP's spread operator:\n\n```php\n$completeHeader = [...$signature->getProtectedHeader(), ...$signature->getHeader()];\n```\n\nIn 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.\n\nThis creates a Time-of-Check/Time-of-Use (TOCTOU) vulnerability:\n1. `HeaderCheckerManager` validates `alg` from the **protected** header\n2. `JWSVerifier` uses `alg` from the **unprotected** header for actual verification\n\nThe same issue exists in `JWEDecrypter.php` (lines 120-124) where `array_merge()` exhibits the same last-wins behavior for `alg` and `enc`.\n\n## Affected Code\n\n**JWSVerifier.php line 144** — Spread operator merge order allows unprotected header to override `alg`:\n```php\n$completeHeader = [...$signature->getProtectedHeader(), ...$signature->getHeader()];\n```\n\n**JWEDecrypter.php lines 120-124** — `array_merge()` with same last-wins behavior:\n```php\n$completeHeader = array_merge(\n    $jwe->getSharedProtectedHeader(),\n    $jwe->getSharedHeader(),\n    $recipient->getHeader()\n);\n```\n\n## Attack Vectors\n\n### Vector A — Mixed key sets (HIGH probability)\nIf 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.\n\n### Vector B — alg ONLY in unprotected header (HIGH probability)\nIf `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.\n\n### Vector C — Direct JWSVerifier usage (HIGH probability)\n`JWSLoader` takes `?HeaderCheckerManager` (nullable). If developers use `JWSVerifier` directly or create `JWSLoader` without a `HeaderCheckerManager`, the duplicate header check never runs.\n\n## Contrast with JWSBuilder (safe)\n\n`JWSBuilder::findSignatureAlgorithm()` (line 196) uses `[...$header, ...$protectedHeader]` where protected wins. It also has `checkDuplicatedHeaderParameters()` (line 218). The JWSVerifier has **neither** safeguard.\n\n## Proof of Concept\n\n```php\n<?php\n// Demonstrate algorithm override via unprotected header\n$protected = [\"alg\" => \"RS256\", \"typ\" => \"JWT\"];\n$unprotected = [\"alg\" => \"HS256\"];\n$merged = [...$protected, ...$unprotected];\n// $merged[\"alg\"] === \"HS256\" — unprotected wins!\n\n// JSON Flattened JWS with algorithm override:\n$maliciousJws = json_encode([\n    'payload' => base64url_encode($payload),\n    'protected' => base64url_encode('{\"alg\":\"RS256\"}'),\n    'header' => ['alg' => 'HS256'],  // OVERRIDE\n    'signature' => base64url_encode($sig),\n]);\n// HeaderCheckerManager validates RS256 from protected header -> PASS\n// JWSVerifier uses HS256 from unprotected header -> attacker's algorithm choice\n```\n\nA full working PoC demonstrating HS512-to-HS256 downgrade with mixed keysets is available upon request.\n\n## Suggested Fix\n\nIn `JWSVerifier::getAlgorithm()`, read `alg` exclusively from the protected header:\n\n```php\nprivate function getAlgorithm(Signature $signature): Algorithm\n{\n    $protectedHeader = $signature->getProtectedHeader();\n    if (! isset($protectedHeader['alg'])) {\n        throw new InvalidArgumentException('The \"alg\" parameter must be in the protected header.');\n    }\n    return $this->signatureAlgorithmManager->get($protectedHeader['alg']);\n}\n```\n\nFor `JWEDecrypter`, reverse the merge order so protected header wins, or extract `alg`/`enc` exclusively from the protected header.\n\n## Résolution\n\nUn 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).\n\n**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é.\n\n**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`.","severity":"high","score":8.1,"published":"2026-06-18T21:09:17.000Z","modified":"2026-09-10T03:50:50.234Z","withdrawn":false,"knownExploited":false,"affected":[{"ecosystem":"Packagist","name":"web-token/jwt-library","ranges":[{"events":[{"introduced":"0"},{"fixed":"3.4.10"}],"type":"ECOSYSTEM"}],"versions":["3.3.0","3.3.1","3.3.2","3.3.3","3.3.4","3.3.5","3.4.0","3.4.1","3.4.2","3.4.3","3.4.4","3.4.5","3.4.6","3.4.7","3.4.8","3.4.9"],"fixed":["3.4.10"]},{"ecosystem":"Packagist","name":"web-token/jwt-library","ranges":[{"events":[{"introduced":"4.0.0"},{"fixed":"4.0.7"}],"type":"ECOSYSTEM"}],"versions":["4.0.0","4.0.1","4.0.2","4.0.3","4.0.4","4.0.5","4.0.6"],"fixed":["4.0.7"]},{"ecosystem":"Packagist","name":"web-token/jwt-library","ranges":[{"events":[{"introduced":"4.1.0"},{"fixed":"4.1.7"}],"type":"ECOSYSTEM"}],"versions":["4.1.0","4.1.1","4.1.2","4.1.3","4.1.4","4.1.5","4.1.6"],"fixed":["4.1.7"]},{"ecosystem":"Packagist","name":"web-token/jwt-bundle","ranges":[{"events":[{"introduced":"0"},{"fixed":"3.4.10"}],"type":"ECOSYSTEM"}],"versions":["3.3.0","3.3.1","3.3.2","3.3.3","3.3.4","3.3.5","3.4.0","3.4.1","3.4.2","3.4.3","3.4.4","3.4.5","3.4.6","3.4.7","3.4.8","3.4.9","v0.0.0","v0.0.1","v0.0.2","v0.1.0","v0.2.0","v0.2.1","v0.2.2","v0.2.3","v0.3.0","v0.3.1","v0.3.2","v0.4.0","v0.4.1","v0.5.0","v0.5.1","v0.5.2","v1.0.0","v1.0.1","v1.0.2","v1.0.3","v1.0.4","v1.0.5","v1.0.6","v1.1.0","v1.1.0-beta1","v1.1.0-beta2","v1.1.0-beta3","v1.1.0-beta4","v1.1.1","v1.1.2","v1.1.3","v1.1.4","v1.2.0","v1.2.1","v1.2.2","v1.2.3","v1.2.4","v1.3.0","v1.3.1","v1.3.10","v1.3.2","v1.3.3","v1.3.4","v1.3.5","v1.3.6","v1.3.7","v1.3.8","v1.3.9","v2.0.0","v2.0.0-alpha1","v2.0.0-alpha2","v2.0.0-alpha3","v2.0.1","v2.0.10","v2.0.11","v2.0.12","v2.0.13","v2.0.2","v2.0.3","v2.0.4","v2.0.5","v2.0.6","v2.0.7","v2.0.8","v2.0.9","v2.1.0","v2.1.1","v2.1.2","v2.1.3","v2.1.4","v2.1.5","v2.1.6","v2.1.7","v2.1.8","v2.1.9","v2.2.0","v2.2.1","v2.2.10","v2.2.11","v2.2.2","v2.2.3","v2.2.4","v2.2.5","v2.2.6","v2.2.7","v2.2.8","v2.2.9","v3.0.1","v3.0.2","v3.0.3","v3.0.4","v3.0.5","v3.0.6","v3.0.7"],"fixed":["3.4.10"]},{"ecosystem":"Packagist","name":"web-token/jwt-bundle","ranges":[{"events":[{"introduced":"4.0.0"},{"fixed":"4.0.7"}],"type":"ECOSYSTEM"}],"versions":["4.0.0","4.0.1","4.0.2","4.0.3","4.0.4","4.0.5","4.0.6"],"fixed":["4.0.7"]},{"ecosystem":"Packagist","name":"web-token/jwt-bundle","ranges":[{"events":[{"introduced":"4.1.0"},{"fixed":"4.1.7"}],"type":"ECOSYSTEM"}],"versions":["4.1.0","4.1.1","4.1.2","4.1.3","4.1.4","4.1.5","4.1.6"],"fixed":["4.1.7"]},{"ecosystem":"Packagist","name":"web-token/jwt-experimental","ranges":[{"events":[{"introduced":"0"},{"fixed":"3.4.10"}],"type":"ECOSYSTEM"}],"versions":["3.3.0","3.3.1","3.3.2","3.3.3","3.3.4","3.3.5","3.4.0","3.4.1","3.4.2","3.4.3","3.4.4","3.4.5","3.4.6","3.4.7","3.4.8","3.4.9"],"fixed":["3.4.10"]},{"ecosystem":"Packagist","name":"web-token/jwt-experimental","ranges":[{"events":[{"introduced":"4.0.0"},{"fixed":"4.0.7"}],"type":"ECOSYSTEM"}],"versions":["4.0.0","4.0.1","4.0.2","4.0.3","4.0.4","4.0.5","4.0.6"],"fixed":["4.0.7"]},{"ecosystem":"Packagist","name":"web-token/jwt-experimental","ranges":[{"events":[{"introduced":"4.1.0"},{"fixed":"4.1.7"}],"type":"ECOSYSTEM"}],"versions":["4.1.0","4.1.1","4.1.2","4.1.3","4.1.4","4.1.5","4.1.6"],"fixed":["4.1.7"]},{"ecosystem":"Packagist","name":"web-token/jwt-framework","ranges":[{"events":[{"introduced":"0"},{"fixed":"3.4.10"}],"type":"ECOSYSTEM"}],"versions":["3.1.0","3.1.1","3.1.2","3.1.3","3.1.4","3.1.5","3.1.6","3.1.7","3.2.0","3.2.1","3.2.10","3.2.2","3.2.3","3.2.4","3.2.5","3.2.6","3.2.7","3.2.8","3.2.9","3.3.0","3.3.0-alpha1","3.3.0-alpha2","3.3.0-alpha3","3.3.1","3.3.2","3.3.3","3.3.4","3.3.5","3.4.0","3.4.1","3.4.2","3.4.3","3.4.4","3.4.5","3.4.6","3.4.7","3.4.8","3.4.9","v0.0.0","v0.0.1","v0.0.2","v0.1.0","v0.2.0","v0.2.1","v0.2.2","v0.2.3","v0.3.0","v0.3.1","v0.3.2","v0.4.0","v0.4.1","v0.5.0","v0.5.1","v0.5.2","v1.0.0","v1.0.1","v1.0.2","v1.0.3","v1.0.4","v1.0.5","v1.0.6","v1.1.0","v1.1.0-beta1","v1.1.0-beta2","v1.1.0-beta3","v1.1.0-beta4","v1.1.1","v1.1.2","v1.1.3","v1.1.4","v1.2.0","v1.2.1","v1.2.2","v1.2.3","v1.2.4","v1.3.0","v1.3.1","v1.3.10","v1.3.2","v1.3.3","v1.3.4","v1.3.5","v1.3.6","v1.3.7","v1.3.8","v1.3.9","v2.0.0","v2.0.0-alpha1","v2.0.0-alpha2","v2.0.0-alpha3","v2.0.1","v2.0.11","v2.0.12","v2.0.13","v2.0.2","v2.0.3","v2.0.4","v2.0.5","v2.0.6","v2.0.7","v2.0.8","v2.0.9","v2.1.0","v2.1.1","v2.1.2","v2.1.3","v2.1.4","v2.1.5","v2.1.6","v2.1.7","v2.1.8","v2.1.9","v2.2.0","v2.2.1","v2.2.10","v2.2.11","v2.2.2","v2.2.3","v2.2.4","v2.2.5","v2.2.6","v2.2.7","v2.2.8","v2.2.9","v3.0.1","v3.0.2","v3.0.3","v3.0.4","v3.0.5","v3.0.6","v3.0.7"],"fixed":["3.4.10"]},{"ecosystem":"Packagist","name":"web-token/jwt-framework","ranges":[{"events":[{"introduced":"4.0.0"},{"fixed":"4.0.7"}],"type":"ECOSYSTEM"}],"versions":["4.0.0","4.0.1","4.0.2","4.0.3","4.0.4","4.0.5","4.0.6"],"fixed":["4.0.7"]},{"ecosystem":"Packagist","name":"web-token/jwt-framework","ranges":[{"events":[{"introduced":"4.1.0"},{"fixed":"4.1.7"}],"type":"ECOSYSTEM"}],"versions":["4.1.0","4.1.1","4.1.2","4.1.3","4.1.4","4.1.5","4.1.6"],"fixed":["4.1.7"]}],"references":["https://github.com/web-token/jwt-framework/security/advisories/GHSA-jc38-x7x8-2xc8","https://github.com/FriendsOfPHP/security-advisories/blob/master/web-token/jwt-library/GHSA-jc38-x7x8-2xc8.yaml","https://github.com/web-token/jwt-framework"],"sources":[{"name":"OSV","id":"GHSA-jc38-x7x8-2xc8","modified":"2026-09-10T03:50:50.234520244Z","url":"https://osv.dev/vulnerability/GHSA-jc38-x7x8-2xc8"}],"assessments":[{"source":"OSV","vector":"CVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N/E:P","score":8.1}]}