NestedTokenLoader: read "cty" from the JWE protected header only

Open Beginner friendly
#757 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
74/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
php

Research direction

Start by reading NestedTokenLoader::checkContentTypeHeader() and the JWE header accessors it uses; compare them with NestedTokenBuilder, which writes cty to the protected header. Change the check to accept cty only from the shared protected header, then review or add coverage for JSON serialization with cty only in an unprotected header. Done means that case is rejected while protected cty continues to work; the issue names no test file.

Written by the indexing model from the issue text.

Description

enhancement
Version(s) affected

All versions since nested tokens were added (1.1.0): 3.4.x, 4.0.x, 4.1.x, 4.2.x, and the 4.3.x and 5.0.x branches.

Description

NestedTokenLoader::checkContentTypeHeader() decides that a JWE carries a nested token from cty, read from the shared protected header, then from the shared unprotected header, then from the per-recipient header. The last two are not covered by the authentication tag of the JWE. A JWE issued without cty in its protected header can therefore be accepted as a nested token once it is serialized in a JSON form with cty in an unprotected member, which needs no key.

The impact is limited:

  • the JWE must decrypt with the application's keys, and the inner JWS must verify against the application's signature keys: nothing is forged or disclosed, and the inner signature is always checked;
  • a protected cty cannot be overridden or removed, because the headers must be disjoint;
  • the JWE loader must accept a JSON serialization;
  • with public-key key management (RSA-OAEP, ECDH-ES), anyone holding a valid JWS can already wrap it in a JWE with a protected cty.

What remains is a confusion between a JWE the issuer marked as a nested JWT and a JWE around a JWS that the issuer did not mark. NestedTokenBuilder always writes cty in the protected header, and JWTs use the compact serialization, where every header parameter is protected, so the unprotected locations are not needed. The rest of the library already reads security-relevant parameters from the protected header only (alg in JWSVerifier; JWEDecrypter does not take alg/enc from the shared unprotected header).

Possible Solution

Read cty from the shared protected header only. This refuses nested tokens from other producers that put cty only in an unprotected member of a JSON serialization. Either do it in 4.3.0 with an UPGRADE note, or accept that case with a deprecation in 4.3.0 and refuse it in 5.0.0.

Applications can enforce it today with a protected-only cty checker among the header checkers of the JWE loader used by the nested token loader, for example new IsEqualChecker('cty', 'JWT') (protected-only by default, case-sensitive), or a CallableChecker doing a case-insensitive comparison.

Additional Context

Same pattern as GHSA-jc38-x7x8-2xc8 (a decision taken on an unprotected header), without its consequence: decryption and signature verification still use the application's keys.

Found by the Anthropic OSS Scanner (ANT-2026-YGYNZW0J).

Dominant language
PHP
Stars
954
Forks
119
Avg merge
48m
Merged PRs (30d)
17

Getting set up

This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from web-token/jwt-framework

All issues in web-token/jwt-framework

Similar issues

More PHP issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.