agentggagentgg
Back to all findings
CRITICALconfirmedalgorithm-confusionalgorithm-confusion2632ba113972

jwt.verify on session cookie does not pin algorithm — RS256→HS256 confusion possible

updateAuthenticatedUsers calls jwt.verify(token, publicKey, cb) without an `algorithms` option, allowing an attacker to forge tokens by signing HS256 with the published RSA public key.

Filelib/insecurity.ts
Lines188196
Confidence
95%
File statusvalidated
Details

In lib/insecurity.ts:

export const updateAuthenticatedUsers = () => (req: Request, res: Response, next: NextFunction) => {
  const token = req.cookies.token || utils.jwtFrom(req)
  if (token) {
    jwt.verify(token, publicKey, (err: Error | null, decoded: any) => {
      if (err === null) {
        if (authenticatedUsers.get(token) === undefined) {
          authenticatedUsers.put(token, decoded)
          res.cookie('token', token)
        }
      }
    })
  }
  next()
}

publicKey is loaded from encryptionkeys/jwt.pub (line 23) — i.e. it is publicly distributed with the application. jwt.verify is invoked without the algorithms: ['RS256'] option, so jsonwebtoken honors the alg header in the token under verification. An attacker can craft a JWT with header {"alg":"HS256"} and sign it using the contents of jwt.pub as the HMAC secret; jsonwebtoken will then treat the public key as a shared HMAC secret and accept the forged token, populating authenticatedUsers with arbitrary identity claims (including role: admin).

The same flaw recurs for the surrounding helpers:

  • L54 isAuthorized = () => expressJwt({ secret: publicKey }) — no algorithms option on the express-jwt middleware that guards the rest of the API.
  • L57 verify = (token) => jws.verify(token, publicKey) — the cast forces the wrong signature; either way no algorithm is pinned.
Proof of concept
  1. Fetch the public key shipped with the app: curl http://target/encryptionkeys/jwt.pub (or recover it from any signed token via jwks-style methods).
  2. Build a forged JWT:
const jwt = require('jsonwebtoken');
const pub = require('fs').readFileSync('jwt.pub', 'utf8');
const forged = jwt.sign({ status: 'success', data: { id: 1, email: 'admin@juice-sh.op', role: 'admin' } }, pub, { algorithm: 'HS256' });
  1. Send the forged token as the token cookie or Authorization: Bearer <forged> header — updateAuthenticatedUsers and isAuthorized will accept it because they do not pin algorithms: ['RS256'].
Impact

Complete authentication bypass / privilege escalation. Any unauthenticated attacker who can read the public key (it is fetched by the frontend at runtime) can mint admin tokens and access every protected endpoint.

Validation
confirmed

The jwt.verify(token, publicKey, cb) call on line ~190 omits the algorithms option, and publicKey is loaded from encryptionkeys/jwt.pub which is shipped with the app and fetched by the frontend at runtime. This is the textbook RS256→HS256 confusion: an attacker signs a forged token with HS256 using the public key bytes as the HMAC secret, and the verifier — lacking algorithm pinning — accepts it, populating authenticatedUsers with attacker-controlled claims. The same pattern repeats in isAuthorized (expressJwt({ secret: publicKey }) cast as any) and verify (jws.verify(token, publicKey)), confirming no algorithm is pinned anywhere. The exploit is reachable from any untrusted request carrying a token cookie or Authorization header.

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, publicKey, cb) call in updateAuthenticatedUsers (and the same omission on isAuthorized's expressJwt({ secret: publicKey }) and verify's jws.verify) does not pass algorithms: ['RS256'], so an attacker can sign a JWT with HS256 using the contents of the publicly-distributed encryptionkeys/jwt.pub as the HMAC secret and have it accepted. The exploit is fully unauthenticated and remote — a single crafted token cookie or Authorization: Bearer header over HTTP is sufficient (AV:N, AC:L, PR:N, UI:N). Because the forged token can claim role: admin and is honored by every protected endpoint, the attacker gains full read/write access to all application data and can disrupt service (C:H/I:H/A:H); the impact remains within the application's own authorization authority, so Scope is Unchanged.

References