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.
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 })— noalgorithmsoption 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.
- Fetch the public key shipped with the app:
curl http://target/encryptionkeys/jwt.pub(or recover it from any signed token viajwks-style methods). - 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' });
- Send the forged token as the
tokencookie orAuthorization: Bearer <forged>header —updateAuthenticatedUsersandisAuthorizedwill accept it because they do not pinalgorithms: ['RS256'].
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.
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.
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.