Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Multiple valid JWT Signatures for HMAC Algorithms (base64 malleability) #365

Open
rb-x opened this issue Dec 29, 2024 · 0 comments
Open

Comments

@rb-x
Copy link

rb-x commented Dec 29, 2024

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)

image

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);

image

@rb-x rb-x changed the title Multiple valid JWT Signatures for HMAC Algorithms Multiple valid JWT Signatures for HMAC Algorithms (base64 malleability) Dec 30, 2024
# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant