JWT Signature Verification Bypass

ID

swift.jwt_signature_verification_bypass

Severity

high

Resource

Cryptography

Language

Swift

Tags

CWE:347, NIST.SP.800-53, OWASP:2021:A03, OWASP:2021:A07, PCI-DSS:6.5.10, PCI-DSS:6.5.6, PCI-DSS:6.5.8

Description

Improper verification of JWT cryptographic signature.

In Swift applications, JWT signature verification bypass commonly occurs when:

  • SwiftJWT library is used with verify: false parameter or .none algorithm

  • Auth0 JWTDecode.swift library is used for security validation (it never verifies signatures)

  • Vapor JWT is configured with .none algorithm or uses JWTParser.parse() without verification

  • JWT tokens are decoded without subsequent signature verification

Rationale

JWT signature verification bypass refers to a scenario where a JSON Web Token, designed to be a secure way to transmit information between parties, is not properly checked for a valid signature.

This can allow attackers to forge tokens, gaining unauthorized access to protected resources or services.

Swift has several popular JWT libraries, each with specific security considerations:


  • SwiftJWT (IBM/Kitura) - Unsafe Patterns

The SwiftJWT library provides multiple ways to decode JWTs, some of which bypass signature verification:

import SwiftJWT

struct MyClaims: Claims {
    let sub: String
    let admin: Bool
}

// VULNERABLE: decode without verification
func validateTokenUnsafe1(_ tokenString: String) throws -> MyClaims {
    // FLAW: Decodes without verifying signature
    let jwt = try JWT<MyClaims>.decode(tokenString)
    return jwt.claims
}

// VULNERABLE: explicitly disable verification
func validateTokenUnsafe2(_ tokenString: String) throws -> MyClaims {
    let key = "secret".data(using: .utf8)!

    // FLAW: verify parameter set to false
    let jwt = try JWT<MyClaims>.decode(
        tokenString,
        algorithm: .hs256(key),
        verify: false
    )
    return jwt.claims
}

// VULNERABLE: using .none algorithm
func signTokenUnsafe(_ claims: MyClaims) throws -> String {
    var jwt = JWT(claims: claims)

    // FLAW: JWTSigner with .none algorithm doesn't sign
    let signer = JWTSigner.none()
    return try jwt.sign(using: signer)
}

An attacker can: - Forge tokens without a valid signature - Modify claims (e.g., change admin: false to admin: true) - Bypass authentication entirely


  • JWTDecode.swift (Auth0) - CRITICAL MISUSE

The Auth0 JWTDecode.swift library is designed only for reading claims from already-trusted tokens. It NEVER verifies signatures:

import JWTDecode

// VULNERABLE: Using JWTDecode for security validation
func validateTokenWithAuth0(_ tokenString: String) throws -> Bool {
    let jwt = try decode(jwt: tokenString)

    // FLAW: JWTDecode NEVER verifies signatures
    // This check is meaningless without signature verification
    guard let exp = jwt.expiresAt, exp > Date() else {
        return false
    }

    // Attacker can forge tokens with any expiration date
    return true
}

// VULNERABLE: Checking claims without verification
func isAdmin(_ tokenString: String) -> Bool {
    guard let jwt = try? decode(jwt: tokenString) else {
        return false
    }

    // FLAW: Reading admin claim without signature verification
    // Attacker can create token with admin=true
    return jwt.claim(name: "admin").boolean ?? false
}

CRITICAL: JWTDecode.swift should ONLY be used for: - Reading claims from tokens you already verified (e.g., via SwiftJWT) - Debugging and development - NEVER for security validation or authentication


  • Vapor JWT - Unsafe Patterns

Vapor’s JWT package can be misconfigured to bypass verification:

import JWT
import Vapor

struct UserPayload: JWTPayload {
    let sub: String
    let exp: ExpirationClaim
    let admin: Bool

    func verify(using signer: JWTSigner) throws {
        try exp.verifyNotExpired()
    }
}

// VULNERABLE: Using .none algorithm
func configureJWTUnsafe(app: Application) {
    // FLAW: Configuring with .none algorithm disables verification
    app.jwt.signers.use(.none())
}

// VULNERABLE: Using JWTParser without verification
func decodeWithoutVerification(_ tokenString: String) throws {
    let bytes = Array(tokenString.utf8)

    // FLAW: JWTParser.parse() does NOT verify signatures
    let (header, payload, signature) = try JWTParser.parse(bytes)

    // Processing unverified JWT data
    processPayload(payload)
}

// VULNERABLE: Skipping signature check
func validateUnsafe(req: Request) throws -> UserPayload {
    // FLAW: If JWT middleware uses .none, this doesn't verify
    return try req.jwt.verify(as: UserPayload.self)
}

Remediation

To remediate the JWT signature verification bypass, ensure that you are properly configuring the JWT parser in use, and always verifying the token signature with a trusted public key or secret.

Furthermore, make sure your JWT libraries are updated to the latest versions, which often address security vulnerabilities and provide enhanced capabilities. Additionally, it’s important to apply similar best practices across all environments where JWTs are used or processed to maintain consistent security assurances.


  • Option 1: SwiftJWT with Proper Verification

import SwiftJWT

struct MyClaims: Claims {
    let sub: String
    let iss: String
    let exp: Date
    let admin: Bool
}

// FIXED: Use JWTVerifier for secure validation
func validateTokenSecure(_ tokenString: String) throws -> MyClaims {
    let secret = getSecretFromEnvironment() // Not hardcoded

    // Create verifier with proper algorithm
    let verifier = JWTVerifier.hs256(key: secret)

    // FIXED: decode with verifier - signatures are verified
    let jwt = try JWT<MyClaims>.decode(tokenString, verifier: verifier)

    // Additional claim validation
    guard jwt.claims.iss == "https://trusted-issuer.com" else {
        throw JWTError.invalidIssuer
    }

    guard jwt.claims.exp > Date() else {
        throw JWTError.expired
    }

    return jwt.claims
}

// FIXED: Proper signing
func signTokenSecure(_ claims: MyClaims) throws -> String {
    var jwt = JWT(claims: claims)
    let secret = getSecretFromEnvironment()

    // FIXED: Use proper algorithm (HS256, RS256, etc.)
    let signer = JWTSigner.hs256(key: secret)
    return try jwt.sign(using: signer)
}

  • Option 2: Vapor JWT with Secure Configuration

import JWT
import Vapor

struct UserPayload: JWTPayload {
    let sub: String
    let exp: ExpirationClaim
    let admin: Bool

    func verify(using signer: JWTSigner) throws {
        // Verify expiration
        try exp.verifyNotExpired()

        // Additional claim validation can go here
    }
}

// FIXED: Configure with proper algorithm
func configureJWTSecure(app: Application) throws {
    let secret = Environment.get("JWT_SECRET")!

    // FIXED: Use secure algorithm (HS256, RS256, etc.)
    app.jwt.signers.use(.hs256(key: secret))
}

// FIXED: Use JWTSigners.verify() for validation
func validateSecure(req: Request, token: String) throws -> UserPayload {
    // FIXED: This verifies the signature
    return try req.application.jwt.signers.verify(token, as: UserPayload.self)
}

// FIXED: Verify in middleware
func jwtMiddleware(req: Request, next: Responder) -> EventLoopFuture<Response> {
    guard let token = req.headers.bearerAuthorization?.token else {
        return req.eventLoop.makeFailedFuture(Abort(.unauthorized))
    }

    do {
        // FIXED: Verify signature before proceeding
        let payload = try req.application.jwt.signers.verify(token, as: UserPayload.self)
        req.auth.login(payload)
        return next.respond(to: req)
    } catch {
        return req.eventLoop.makeFailedFuture(Abort(.unauthorized))
    }
}

  • Option 3: Never Use JWTDecode for Validation

import JWTDecode
import SwiftJWT

// WRONG: Using JWTDecode for validation
func validateWrong(_ tokenString: String) -> Bool {
    guard let jwt = try? decode(jwt: tokenString) else {
        return false
    }
    return jwt.expired == false  // WRONG: No signature verification!
}

// CORRECT: Use JWTDecode only after verification
func validateCorrect(_ tokenString: String) throws -> [String: Any] {
    let secret = getSecretFromEnvironment()
    let verifier = JWTVerifier.hs256(key: secret)

    // STEP 1: Verify with SwiftJWT
    _ = try JWT<ClaimSet>.decode(tokenString, verifier: verifier)

    // STEP 2: Now safe to read claims with JWTDecode (optional)
    let jwt = try decode(jwt: tokenString)
    return jwt.body
}

Which JWT Library Should I Use?

For Production Security: - ✅ SwiftJWT - Full-featured, supports verification - ✅ Vapor JWT - Well-integrated with Vapor framework - ❌ JWTDecode.swift - NEVER for security validation

Algorithm Recommendations: - ✅ HS256/HS384/HS512 - HMAC with secure secrets - ✅ RS256/RS384/RS512 - RSA with public/private keys - ✅ ES256/ES384/ES512 - ECDSA with public/private keys - ❌ none - NEVER use in production

Key Security Practices

  1. Always verify signatures: Use JWTVerifier or JWTSigners.verify()

  2. Never use .none algorithm: Always specify a secure algorithm

  3. Don’t use JWTDecode for validation: Only for reading already-verified tokens

  4. Validate claims: Check issuer, expiration, audience, etc.

  5. Use environment variables for secrets: Never hardcode keys

  6. Keep libraries updated: Security fixes are released regularly

  7. Verify algorithm in header: Prevent algorithm substitution attacks

  8. Use HTTPS: Protect tokens in transit

  9. Short expiration times: Limit token validity window

  10. Implement token revocation: Handle compromised tokens

iOS/macOS Specific Considerations

Keychain Storage: - Store JWT secrets in Keychain, not UserDefaults - Use kSecAttrAccessibleWhenUnlocked or stricter

App Transport Security: - Ensure token endpoints use HTTPS - Validate server certificates

Background Processing: - Re-verify tokens when app returns from background - Check expiration on app launch

Configuration

This is a regular (non-tainting) detector that inspects JWT API calls for signature verification bypass patterns.

References