agentggagentgg
Back to all findings
CRITICALconfirmedalgorithm-confusionalgorithm-confusion272f73f32023

jwt.verify called without algorithms option enables RS256→HS256 algorithm confusion

jwtChallenge() calls jwt.verify with the RSA public key and no algorithms option, so a token whose header advertises HS256 will be verified using the public key as an HMAC secret — accepting attacker-forged tokens.

Fileroutes/verify.ts
Lines119125
Confidence
85%
File statusvalidated
Details

In routes/verify.ts:

function jwtChallenge (challenge: Challenge, req: Request, algorithm: string, email: string | RegExp) {
  const token = utils.jwtFrom(req)
  if (token) {
    const decoded = jws.decode(token) ? jwt.decode(token) : null
    if (decoded === null || typeof decoded === 'string') {
      return
    }
    jwt.verify(token, security.publicKey, (err: jwt.VerifyErrors | null) => {
      if (err === null) {
        challengeUtils.solveIf(challenge, () => {
          return hasAlgorithm(token, algorithm) && hasEmail(decoded as { data: { email: string } }, email)
        })
      }
    })
  }
}

security.publicKey is loaded from encryptionkeys/jwt.pub (RSA public key, see lib/insecurity.ts L22), and tokens are normally signed with RS256 (lib/insecurity.ts L56). However, jwt.verify is called WITHOUT an algorithms: [...] option — meaning jsonwebtoken will honour whichever algorithm the inbound JWT header declares. Two specific attacks succeed:

  1. alg: none — the verifier accepts a fully-unsigned token (matches the jwtUnsignedChallenge detection path at line 96).
  2. alg: HS256 — the verifier treats the published RSA public key as an HMAC secret. An attacker who has the public key (it is shipped in the application) can sign arbitrary payloads with HS256 and the public key bytes; the verify call passes (matches the jwtForgedChallenge detection path at line 99).

The safe form is jwt.verify(token, publicKey, { algorithms: ['RS256'] }). This function in particular only gates challenge-solving, but the same insecure verification pattern lives in lib/insecurity.ts L189 (updateAuthenticatedUsers) where it is consulted to mint a session cookie, broadening the impact.

Proof of concept
  1. Read the bundled RSA public key from encryptionkeys/jwt.pub.
  2. Construct a JWT header { "alg": "HS256", "typ": "JWT" } and a payload { "data": { "email": "rsa_lord@juice-sh.op", ... } }.
  3. HMAC-SHA256 sign the header.payload using the raw PEM bytes of the public key as the secret.
  4. Send the resulting token in Authorization: Bearer <token> to any route processed by jwtChallenges. jwt.verify accepts the HS256 signature because no algorithm allowlist was provided.
Impact

Attacker-forged JWTs pass signature verification. In the immediate context this marks the jwtForged/jwtUnsigned challenges as solved; combined with the identical pattern in lib/insecurity.ts (updateAuthenticatedUsers) it leads to authentication bypass via algorithm confusion, allowing impersonation of any user including administrators.

Validation
confirmed

The jwt.verify(token, security.publicKey, callback) call at line 119 omits the algorithms option, which causes jsonwebtoken to honor the algorithm declared in the inbound JWT header. With an RSA public key as the secret, an attacker can forge tokens signed via HS256 using the published public key bytes as the HMAC secret (or send alg: none) and verification will succeed — matching the documented behavior of the library. The function is reached by untrusted input via the jwtChallenges middleware (token taken from the request through utils.jwtFrom(req)). Per the scope rules ("Treat every finding as if this were a real production application"), the intentionally vulnerable nature of Juice Shop does not downgrade this.

CVSS 3.1
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Base score: 9.8 · CRITICAL

The jwt.verify(token, security.publicKey, ...) call in jwtChallenge() omits the algorithms allowlist, so an unauthenticated attacker can submit an HS256-signed token whose HMAC key is the publicly shipped encryptionkeys/jwt.pub and the verifier will accept it; alg: none is similarly accepted. No authentication, user interaction, or special preconditions are required — the attacker just sends a crafted Authorization: Bearer header over the network. The finding explicitly notes the identical insecure verification pattern is used in lib/insecurity.ts updateAuthenticatedUsers to mint session cookies, so the worst plausible exploitation is full authentication bypass / admin impersonation, yielding total C/I/A impact within the application. Scope remains Unchanged because the impact stays within the same application's security authority rather than crossing into a different component.

References