-
Notifications
You must be signed in to change notification settings - Fork 248
Open
Description
When using HMAC algorithms (HS256, HS384, HS512), python-jose and PyJWT exhibit different signature validation behavior compared to Node's jsonwebtoken library. Multiple valid signature endings are accepted when modifying the last character.
TLDR: We have a base64 malleability which could be problematic for revocation that relies on the whole jwt instead of jti.
Here's a minimal reproduction script:
from jose import jwt as jose_jwt
import jwt as pyjwt
import string
import uuid
from datetime import datetime, timedelta
# jose 3.3.0
# pyjwt 2.10.1
def test_jwt_signatures(algorithm):
payload = {
"sub": str(uuid.uuid4()),
"exp": int((datetime.now() + timedelta(hours=1)).timestamp()),
"jti": str(uuid.uuid4())
}
secret = "test_secret_key"
jose_token = jose_jwt.encode(payload, secret, algorithm=algorithm)
pyjwt_token = pyjwt.encode(payload, secret, algorithm=algorithm)
print(f"\n=== Testing {algorithm} ===")
print("\nPython-JOSE:")
print(f"JWT: {jose_token}")
print(f"Original signature ends with: {jose_token[-1]}")
valid_chars_jose = []
base64_chars = string.ascii_letters + string.digits + '-_'
for c in base64_chars:
modified = jose_token[:-1] + c
try:
jose_jwt.decode(modified, secret, algorithms=[algorithm])
valid_chars_jose.append(c)
except jose_jwt.JWTError:
pass
if valid_chars_jose:
print(f"Found {len(valid_chars_jose)} valid endings: {valid_chars_jose}")
print("\nPyJWT:")
print(f"JWT: {pyjwt_token}")
print(f"Original signature ends with: {pyjwt_token[-1]}")
valid_chars_pyjwt = []
for c in base64_chars:
modified = pyjwt_token[:-1] + c
try:
pyjwt.decode(modified, secret, algorithms=[algorithm])
valid_chars_pyjwt.append(c)
except pyjwt.InvalidSignatureError:
pass
if valid_chars_pyjwt:
print(f"Found {len(valid_chars_pyjwt)} valid endings: {valid_chars_pyjwt}")
if __name__ == "__main__":
for alg in ["HS256", "HS384", "HS512"]:
test_jwt_signatures(alg)
NodeJS JWT
const jwt = require('jsonwebtoken');
const { v4: uuidv4 } = require('uuid');
// jsonwebtoken: 9.0.2
function testJwtSignature(algorithm) {
const payload = {
sub: uuidv4(),
exp: Math.floor(Date.now() / 1000) + 3600,
jti: uuidv4()
};
const secret = 'test_secret_key';
const token = jwt.sign(payload, secret, { algorithm });
const origSig = token.slice(-1);
console.log(`\n=== Testing ${algorithm} ===`);
console.log(`JWT: ${token}`);
console.log(`Original signature ends with: ${origSig}`);
const validChars = [];
const base64chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
for (const c of base64chars) {
const modified = token.slice(0, -1) + c;
try {
jwt.verify(modified, secret, { algorithms: [algorithm] });
validChars.push(c);
} catch (err) {}
}
if (validChars.length > 0) {
console.log(`Found ${validChars.length} valid endings: ${JSON.stringify(validChars)}`);
}
}
const algorithms = ['HS256', 'HS384', 'HS512'];
algorithms.forEach(testJwtSignature);
Metadata
Metadata
Assignees
Labels
No labels