{"id":"GHSA-jc38-x7x8-2xc8","summary":"PHP JWT Framework: JWSVerifier uses algorithm from unprotected header, enabling algorithm confusion attacks","details":"## 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-\u003egetProtectedHeader(), ...$signature-\u003egetHeader()];\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-\u003egetProtectedHeader(), ...$signature-\u003egetHeader()];\n```\n\n**JWEDecrypter.php lines 120-124** — `array_merge()` with same last-wins behavior:\n```php\n$completeHeader = array_merge(\n    $jwe-\u003egetSharedProtectedHeader(),\n    $jwe-\u003egetSharedHeader(),\n    $recipient-\u003egetHeader()\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\u003c?php\n// Demonstrate algorithm override via unprotected header\n$protected = [\"alg\" =\u003e \"RS256\", \"typ\" =\u003e \"JWT\"];\n$unprotected = [\"alg\" =\u003e \"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' =\u003e base64url_encode($payload),\n    'protected' =\u003e base64url_encode('{\"alg\":\"RS256\"}'),\n    'header' =\u003e ['alg' =\u003e 'HS256'],  // OVERRIDE\n    'signature' =\u003e base64url_encode($sig),\n]);\n// HeaderCheckerManager validates RS256 from protected header -\u003e PASS\n// JWSVerifier uses HS256 from unprotected header -\u003e 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-\u003egetProtectedHeader();\n    if (! isset($protectedHeader['alg'])) {\n        throw new InvalidArgumentException('The \"alg\" parameter must be in the protected header.');\n    }\n    return $this-\u003esignatureAlgorithmManager-\u003eget($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`.","modified":"2026-09-10T03:50:50.234520244Z","published":"2026-06-18T21:09:17Z","database_specific":{"cwe_ids":["CWE-345"],"severity":"HIGH","github_reviewed":true,"github_reviewed_at":"2026-06-18T21:09:17Z","nvd_published_at":null},"references":[{"type":"WEB","url":"https://github.com/web-token/jwt-framework/security/advisories/GHSA-jc38-x7x8-2xc8"},{"type":"WEB","url":"https://github.com/FriendsOfPHP/security-advisories/blob/master/web-token/jwt-library/GHSA-jc38-x7x8-2xc8.yaml"},{"type":"PACKAGE","url":"https://github.com/web-token/jwt-framework"}],"affected":[{"package":{"name":"web-token/jwt-library","ecosystem":"Packagist","purl":"pkg:composer/web-token/jwt-library"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"3.4.10"}]}],"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"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/06/GHSA-jc38-x7x8-2xc8/GHSA-jc38-x7x8-2xc8.json"}},{"package":{"name":"web-token/jwt-library","ecosystem":"Packagist","purl":"pkg:composer/web-token/jwt-library"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"4.0.0"},{"fixed":"4.0.7"}]}],"versions":["4.0.0","4.0.1","4.0.2","4.0.3","4.0.4","4.0.5","4.0.6"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/06/GHSA-jc38-x7x8-2xc8/GHSA-jc38-x7x8-2xc8.json"}},{"package":{"name":"web-token/jwt-library","ecosystem":"Packagist","purl":"pkg:composer/web-token/jwt-library"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"4.1.0"},{"fixed":"4.1.7"}]}],"versions":["4.1.0","4.1.1","4.1.2","4.1.3","4.1.4","4.1.5","4.1.6"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/06/GHSA-jc38-x7x8-2xc8/GHSA-jc38-x7x8-2xc8.json"}},{"package":{"name":"web-token/jwt-bundle","ecosystem":"Packagist","purl":"pkg:composer/web-token/jwt-bundle"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"3.4.10"}]}],"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"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/06/GHSA-jc38-x7x8-2xc8/GHSA-jc38-x7x8-2xc8.json"}},{"package":{"name":"web-token/jwt-bundle","ecosystem":"Packagist","purl":"pkg:composer/web-token/jwt-bundle"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"4.0.0"},{"fixed":"4.0.7"}]}],"versions":["4.0.0","4.0.1","4.0.2","4.0.3","4.0.4","4.0.5","4.0.6"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/06/GHSA-jc38-x7x8-2xc8/GHSA-jc38-x7x8-2xc8.json"}},{"package":{"name":"web-token/jwt-bundle","ecosystem":"Packagist","purl":"pkg:composer/web-token/jwt-bundle"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"4.1.0"},{"fixed":"4.1.7"}]}],"versions":["4.1.0","4.1.1","4.1.2","4.1.3","4.1.4","4.1.5","4.1.6"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/06/GHSA-jc38-x7x8-2xc8/GHSA-jc38-x7x8-2xc8.json"}},{"package":{"name":"web-token/jwt-experimental","ecosystem":"Packagist","purl":"pkg:composer/web-token/jwt-experimental"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"3.4.10"}]}],"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"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/06/GHSA-jc38-x7x8-2xc8/GHSA-jc38-x7x8-2xc8.json"}},{"package":{"name":"web-token/jwt-experimental","ecosystem":"Packagist","purl":"pkg:composer/web-token/jwt-experimental"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"4.0.0"},{"fixed":"4.0.7"}]}],"versions":["4.0.0","4.0.1","4.0.2","4.0.3","4.0.4","4.0.5","4.0.6"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/06/GHSA-jc38-x7x8-2xc8/GHSA-jc38-x7x8-2xc8.json"}},{"package":{"name":"web-token/jwt-experimental","ecosystem":"Packagist","purl":"pkg:composer/web-token/jwt-experimental"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"4.1.0"},{"fixed":"4.1.7"}]}],"versions":["4.1.0","4.1.1","4.1.2","4.1.3","4.1.4","4.1.5","4.1.6"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/06/GHSA-jc38-x7x8-2xc8/GHSA-jc38-x7x8-2xc8.json"}},{"package":{"name":"web-token/jwt-framework","ecosystem":"Packagist","purl":"pkg:composer/web-token/jwt-framework"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"3.4.10"}]}],"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"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/06/GHSA-jc38-x7x8-2xc8/GHSA-jc38-x7x8-2xc8.json"}},{"package":{"name":"web-token/jwt-framework","ecosystem":"Packagist","purl":"pkg:composer/web-token/jwt-framework"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"4.0.0"},{"fixed":"4.0.7"}]}],"versions":["4.0.0","4.0.1","4.0.2","4.0.3","4.0.4","4.0.5","4.0.6"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/06/GHSA-jc38-x7x8-2xc8/GHSA-jc38-x7x8-2xc8.json"}},{"package":{"name":"web-token/jwt-framework","ecosystem":"Packagist","purl":"pkg:composer/web-token/jwt-framework"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"4.1.0"},{"fixed":"4.1.7"}]}],"versions":["4.1.0","4.1.1","4.1.2","4.1.3","4.1.4","4.1.5","4.1.6"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/06/GHSA-jc38-x7x8-2xc8/GHSA-jc38-x7x8-2xc8.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V4","score":"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"}]}