GHSA-jc38-x7x8-2xc8: PHP JWT Framework JWSVerifier Uses Algorithm From Unprotected Vulnerability

HIGH
CWE-345
Published
Advisory Summary

GHSA-jc38-x7x8-2xc8 is a HIGH jwsverifier uses algorithm from unprotected vulnerability (CWE-345: Insufficient Verification of Data Authenticity) affecting PHP JWT Framework libraries in the Packagist ecosystem. The issue involves jwsverifier uses algorithm from unprotected header, enabling algorithm confusion attacks. Remediation is available by upgrading affected packages to version 4.1.7, 4.0.7, 3.4.10.

GHSA-jc38-x7x8-2xc8 at a Glance

CVE Identifier N/A
GHSA Identifier GHSA-jc38-x7x8-2xc8
Severity HIGH
CVSS v3.1 Base Score N/A
CWE Classification CWE-345: Insufficient Verification of Data Authenticity
Affected Product PHP JWT Framework
Package Ecosystem Packagist
Published Date
Last Updated

How to Fix GHSA-jc38-x7x8-2xc8

To remediate this vulnerability, upgrade all affected PHP JWT Framework dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

web-token/jwt-framework fixed: 4.1.7
Upgrade web-token/jwt-framework to 4.1.7
web-token/jwt-framework fixed: 4.0.7
Upgrade web-token/jwt-framework to 4.0.7
web-token/jwt-library fixed: 3.4.10
Upgrade web-token/jwt-library to 3.4.10
web-token/jwt-library fixed: 4.0.7
Upgrade web-token/jwt-library to 4.0.7
web-token/jwt-library fixed: 4.1.7
Upgrade web-token/jwt-library to 4.1.7
web-token/jwt-bundle fixed: 3.4.10
Upgrade web-token/jwt-bundle to 3.4.10
web-token/jwt-bundle fixed: 4.0.7
Upgrade web-token/jwt-bundle to 4.0.7
web-token/jwt-bundle fixed: 4.1.7
Upgrade web-token/jwt-bundle to 4.1.7
web-token/jwt-experimental fixed: 3.4.10
Upgrade web-token/jwt-experimental to 3.4.10
web-token/jwt-experimental fixed: 4.0.7
Upgrade web-token/jwt-experimental to 4.0.7
web-token/jwt-experimental fixed: 4.1.7
Upgrade web-token/jwt-experimental to 4.1.7
web-token/jwt-framework fixed: 3.4.10
Upgrade web-token/jwt-framework to 3.4.10
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for web-token/jwt-framework

How to Check if You Are Affected

To determine whether your application is affected by GHSA-jc38-x7x8-2xc8, inspect your project dependencies for PHP JWT Framework:

inspect dependencies for web-token/jwt-framework

Analyze the terminal output to locate any version of PHP JWT Framework in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 4.1.7), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-345: Insufficient Verification of Data Authenticity, where the product does not sufficiently verify the origin or authenticity of data, in a way that causes it to accept invalid data. In PHP JWT Framework, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## 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: 1. `HeaderCheckerManager` validates `alg` from the **protected** header 2. `JWSVerifier` uses `alg` from 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 choice ``` A 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`.

GHSA-jc38-x7x8-2xc8 Affected PHP JWT Framework Versions

12 component(s)
Fixed Versions:
4.1.74.0.73.4.10
Product / Package Ecosystem Affected Versions Fixed Version
web-token/jwt-framework
Packagist
≥ 4.1.0
< 4.1.7
4.1.7
web-token/jwt-framework
Packagist
≥ 4.0.0
< 4.0.7
4.0.7
web-token/jwt-library
Packagist
< 3.4.10
3.4.10
web-token/jwt-library
Packagist
≥ 4.0.0
< 4.0.7
4.0.7
web-token/jwt-library
Packagist
≥ 4.1.0
< 4.1.7
4.1.7
web-token/jwt-bundle
Packagist
< 3.4.10
3.4.10
web-token/jwt-bundle
Packagist
≥ 4.0.0
< 4.0.7
4.0.7
web-token/jwt-bundle
Packagist
≥ 4.1.0
< 4.1.7
4.1.7
web-token/jwt-experimental
Packagist
< 3.4.10
3.4.10
web-token/jwt-experimental
Packagist
≥ 4.0.0
< 4.0.7
4.0.7
web-token/jwt-experimental
Packagist
≥ 4.1.0
< 4.1.7
4.1.7
web-token/jwt-framework
Packagist
< 3.4.10
3.4.10

References

Frequently Asked Questions

What is GHSA-jc38-x7x8-2xc8?
GHSA-jc38-x7x8-2xc8 is a security vulnerability in PHP JWT Framework involving JWSVerifier uses algorithm from unprotected header, enabling algorithm confusion attacks under CWE-345: Insufficient Verification of Data Authenticity.
Which PHP JWT Framework versions are affected?
Affected versions include releases prior to 4.1.7, 4.0.7, 3.4.10 across affected packages: web-token/jwt-framework, web-token/jwt-library, web-token/jwt-bundle, web-token/jwt-experimental.
What version fixes GHSA-jc38-x7x8-2xc8?
Upgrading to 4.1.7, 4.0.7, 3.4.10 (or the latest patched release for your respective branch) resolves the vulnerability.
Is web-token/jwt-framework affected?
Yes, web-token/jwt-framework is affected in releases prior to 4.1.7, 4.0.7, 3.4.10. Upgrade your dependency to 4.1.7, 4.0.7, 3.4.10 or later.
Is web-token/jwt-library affected?
Yes, web-token/jwt-library is affected in releases prior to 3.4.10, 4.0.7, 4.1.7. Upgrade your dependency to 3.4.10, 4.0.7, 4.1.7 or later.
What CWE is associated with GHSA-jc38-x7x8-2xc8?
GHSA-jc38-x7x8-2xc8 is classified under CWE-345: Insufficient Verification of Data Authenticity (The product does not sufficiently verify the origin or authenticity of data, in a way that causes it to accept invalid data.).
How can I check Packagist dependencies for GHSA-jc38-x7x8-2xc8?
Run the dependency tree command: 'inspect dependencies for web-token/jwt-framework'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-jc38-x7x8-2xc8",
  "modified" : "2026-07-06T16:29:56Z",
  "published" : "2026-06-18T21:09:17Z",
  "aliases" : [ ],
  "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->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" : [ {
    "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"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Packagist",
      "name" : "web-token/jwt-library"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "3.4.10"
      } ]
    } ]
  }, {
    "package" : {
      "ecosystem" : "Packagist",
      "name" : "web-token/jwt-library"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "4.0.0"
      }, {
        "fixed" : "4.0.7"
      } ]
    } ]
  }, {
    "package" : {
      "ecosystem" : "Packagist",
      "name" : "web-token/jwt-library"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "4.1.0"
      }, {
        "fixed" : "4.1.7"
      } ]
    } ]
  }, {
    "package" : {
      "ecosystem" : "Packagist",
      "name" : "web-token/jwt-bundle"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "3.4.10"
      } ]
    } ]
  }, {
    "package" : {
      "ecosystem" : "Packagist",
      "name" : "web-token/jwt-bundle"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "4.0.0"
      }, {
        "fixed" : "4.0.7"
      } ]
    } ]
  }, {
    "package" : {
      "ecosystem" : "Packagist",
      "name" : "web-token/jwt-bundle"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "4.1.0"
      }, {
        "fixed" : "4.1.7"
      } ]
    } ]
  }, {
    "package" : {
      "ecosystem" : "Packagist",
      "name" : "web-token/jwt-experimental"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "3.4.10"
      } ]
    } ]
  }, {
    "package" : {
      "ecosystem" : "Packagist",
      "name" : "web-token/jwt-experimental"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "4.0.0"
      }, {
        "fixed" : "4.0.7"
      } ]
    } ]
  }, {
    "package" : {
      "ecosystem" : "Packagist",
      "name" : "web-token/jwt-experimental"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "4.1.0"
      }, {
        "fixed" : "4.1.7"
      } ]
    } ]
  }, {
    "package" : {
      "ecosystem" : "Packagist",
      "name" : "web-token/jwt-framework"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "3.4.10"
      } ]
    } ]
  }, {
    "package" : {
      "ecosystem" : "Packagist",
      "name" : "web-token/jwt-framework"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "4.0.0"
      }, {
        "fixed" : "4.0.7"
      } ]
    } ]
  }, {
    "package" : {
      "ecosystem" : "Packagist",
      "name" : "web-token/jwt-framework"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "4.1.0"
      }, {
        "fixed" : "4.1.7"
      } ]
    } ]
  } ],
  "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"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-345" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-06-18T21:09:17Z",
    "nvd_published_at" : null
  }
}