Skip to content

Commit eead1d8

Browse files
committed
1 parent fd8ff64 commit eead1d8

1 file changed

Lines changed: 156 additions & 4 deletions

File tree

advisories/github-reviewed/2026/06/GHSA-jc38-x7x8-2xc8/GHSA-jc38-x7x8-2xc8.json

Lines changed: 156 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
{
22
"schema_version": "1.4.0",
33
"id": "GHSA-jc38-x7x8-2xc8",
4-
"modified": "2026-06-18T21:09:18Z",
4+
"modified": "2026-06-18T21:09:20Z",
55
"published": "2026-06-18T21:09:17Z",
66
"aliases": [],
77
"summary": "PHP JWT Framework: JWSVerifier uses algorithm from unprotected header, enabling algorithm confusion attacks",
88
"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->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`.",
99
"severity": [
1010
{
1111
"type": "CVSS_V4",
12-
"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"
12+
"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"
1313
}
1414
],
1515
"affected": [
@@ -26,7 +26,159 @@
2626
"introduced": "0"
2727
},
2828
{
29-
"last_affected": "4.2.99"
29+
"fixed": "3.4.10"
30+
}
31+
]
32+
}
33+
]
34+
},
35+
{
36+
"package": {
37+
"ecosystem": "Packagist",
38+
"name": "web-token/jwt-framework"
39+
},
40+
"ranges": [
41+
{
42+
"type": "ECOSYSTEM",
43+
"events": [
44+
{
45+
"introduced": "4.0.0"
46+
},
47+
{
48+
"fixed": "4.0.7"
49+
}
50+
]
51+
}
52+
]
53+
},
54+
{
55+
"package": {
56+
"ecosystem": "Packagist",
57+
"name": "web-token/jwt-framework"
58+
},
59+
"ranges": [
60+
{
61+
"type": "ECOSYSTEM",
62+
"events": [
63+
{
64+
"introduced": "4.1.0"
65+
},
66+
{
67+
"fixed": "4.1.7"
68+
}
69+
]
70+
}
71+
]
72+
},
73+
{
74+
"package": {
75+
"ecosystem": "Packagist",
76+
"name": "web-token/jwt-bundle"
77+
},
78+
"ranges": [
79+
{
80+
"type": "ECOSYSTEM",
81+
"events": [
82+
{
83+
"introduced": "0"
84+
},
85+
{
86+
"fixed": "3.4.10"
87+
}
88+
]
89+
}
90+
]
91+
},
92+
{
93+
"package": {
94+
"ecosystem": "Packagist",
95+
"name": "web-token/jwt-bundle"
96+
},
97+
"ranges": [
98+
{
99+
"type": "ECOSYSTEM",
100+
"events": [
101+
{
102+
"introduced": "4.0.0"
103+
},
104+
{
105+
"fixed": "4.0.7"
106+
}
107+
]
108+
}
109+
]
110+
},
111+
{
112+
"package": {
113+
"ecosystem": "Packagist",
114+
"name": "web-token/jwt-bundle"
115+
},
116+
"ranges": [
117+
{
118+
"type": "ECOSYSTEM",
119+
"events": [
120+
{
121+
"introduced": "4.1.0"
122+
},
123+
{
124+
"fixed": "4.1.7"
125+
}
126+
]
127+
}
128+
]
129+
},
130+
{
131+
"package": {
132+
"ecosystem": "Packagist",
133+
"name": "web-token/jwt-experimental"
134+
},
135+
"ranges": [
136+
{
137+
"type": "ECOSYSTEM",
138+
"events": [
139+
{
140+
"introduced": "0"
141+
},
142+
{
143+
"fixed": "3.4.10"
144+
}
145+
]
146+
}
147+
]
148+
},
149+
{
150+
"package": {
151+
"ecosystem": "Packagist",
152+
"name": "web-token/jwt-experimental"
153+
},
154+
"ranges": [
155+
{
156+
"type": "ECOSYSTEM",
157+
"events": [
158+
{
159+
"introduced": "4.0.0"
160+
},
161+
{
162+
"fixed": "4.0.7"
163+
}
164+
]
165+
}
166+
]
167+
},
168+
{
169+
"package": {
170+
"ecosystem": "Packagist",
171+
"name": "web-token/jwt-experimental"
172+
},
173+
"ranges": [
174+
{
175+
"type": "ECOSYSTEM",
176+
"events": [
177+
{
178+
"introduced": "4.1.0"
179+
},
180+
{
181+
"fixed": "4.1.7"
30182
}
31183
]
32184
}
@@ -108,7 +260,7 @@
108260
"cwe_ids": [
109261
"CWE-345"
110262
],
111-
"severity": "HIGH",
263+
"severity": "CRITICAL",
112264
"github_reviewed": true,
113265
"github_reviewed_at": "2026-06-18T21:09:17Z",
114266
"nvd_published_at": null

0 commit comments

Comments
 (0)