Skip to content

feat!: v4 — TypeScript port, zero deps, timing-safe HMAC, fix CVE-202… - #34

Merged
joaquimserafim merged 1 commit into
mainfrom
v4
May 27, 2026
Merged

feat!: v4 — TypeScript port, zero deps, timing-safe HMAC, fix CVE-202…#34
joaquimserafim merged 1 commit into
mainfrom
v4

Conversation

@joaquimserafim

Copy link
Copy Markdown
Owner

…3-48238

Fixes CVE-2023-48238 (JWT algorithm confusion, GHSA-4xw9-cx39-r355). The v3 decode trusted whatever header.alg the token declared, allowing an attacker with the server's RSA public key to craft a token with alg:HS256 whose signature is an HMAC keyed by that public key. v4 now classifies the key handed in (PEM-encoded vs plain secret) and refuses to verify when it doesn't match the algorithm family. The same guard runs on encode to catch developer misuse. An optional { algorithms: string[] } allowlist on decode adds a second layer for safety-conscious callers.

Other security and quality improvements:

  • HMAC verify uses crypto.timingSafeEqual on length-checked Buffers instead of the v3 string === compare.
  • alg: "none" continues to be rejected on both encode and decode.
  • Defensive parsing: tokens whose decoded header is not a JSON object, or whose alg is not a string, are rejected.

Tooling:

  • TypeScript source under src/, dual ESM+CJS+types output via tsup
  • vitest replaces mocha + nyc (43 tests, 100% coverage)
  • Biome replaces standard (tabs, width 4)
  • GitHub Actions matrix on Node 20/22/24 replaces Travis
  • pnpm as the package manager, engines >=18

Runtime deps dropped (all 4):

  • base64-url → Buffer.from(s, "base64url") / .toString("base64url")
  • is.object → inline plain-object brand check
  • json-parse-safe → native try { JSON.parse } catch
  • xtend → object spread (defaultHeader still wins on conflict)

Public API:

  • encode(key, data, [algorithm], [cb]) — unchanged
  • decode(key, token, [options], [cb]) — options is new and optional
  • getAlgorithms() — unchanged
  • JWTError — now a real class extending Error
  • Named exports; require('json-web-token') returns { encode, decode, getAlgorithms, JWTError } as in v3

BREAKING CHANGE: engines bumped to Node >=18.
BREAKING CHANGE: HS256 paired with a PEM key (or any RS256 paired with a plain secret) is now rejected by both encode and decode. Tokens crafted to exploit CVE-2023-48238 will no longer verify.

…3-48238

Fixes CVE-2023-48238 (JWT algorithm confusion, GHSA-4xw9-cx39-r355). The
v3 decode trusted whatever header.alg the token declared, allowing an
attacker with the server's RSA public key to craft a token with
alg:HS256 whose signature is an HMAC keyed by that public key. v4 now
classifies the key handed in (PEM-encoded vs plain secret) and refuses
to verify when it doesn't match the algorithm family. The same guard
runs on encode to catch developer misuse. An optional
{ algorithms: string[] } allowlist on decode adds a second layer for
safety-conscious callers.

Other security and quality improvements:
- HMAC verify uses crypto.timingSafeEqual on length-checked Buffers
  instead of the v3 string === compare.
- alg: "none" continues to be rejected on both encode and decode.
- Defensive parsing: tokens whose decoded header is not a JSON object,
  or whose alg is not a string, are rejected.

Tooling:
- TypeScript source under src/, dual ESM+CJS+types output via tsup
- vitest replaces mocha + nyc (43 tests, 100% coverage)
- Biome replaces standard (tabs, width 4)
- GitHub Actions matrix on Node 20/22/24 replaces Travis
- pnpm as the package manager, engines >=18

Runtime deps dropped (all 4):
- base64-url → Buffer.from(s, "base64url") / .toString("base64url")
- is.object → inline plain-object brand check
- json-parse-safe → native try { JSON.parse } catch
- xtend → object spread (defaultHeader still wins on conflict)

Public API:
- encode(key, data, [algorithm], [cb]) — unchanged
- decode(key, token, [options], [cb]) — options is new and optional
- getAlgorithms() — unchanged
- JWTError — now a real class extending Error
- Named exports; require('json-web-token') returns
  { encode, decode, getAlgorithms, JWTError } as in v3

BREAKING CHANGE: engines bumped to Node >=18.
BREAKING CHANGE: HS256 paired with a PEM key (or any RS256 paired with
a plain secret) is now rejected by both encode and decode. Tokens
crafted to exploit CVE-2023-48238 will no longer verify.
@joaquimserafim
joaquimserafim merged commit 69ce782 into main May 27, 2026
8 checks passed
@joaquimserafim
joaquimserafim deleted the v4 branch May 27, 2026 01:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant