Parsing user-controlled JWTs without signature verification

Parsing user-controlled JWTs without signature verification

Description

Parser.ParseUnverified decodes a JWT's header and claims without checking its signature. The golang-jwt/jwt/v5 documentation also states that the returned Token.Valid is false.

Trusting these decoded claims for authentication or authorization lets an attacker forge values such as sub, roles, and permissions. This differs from a limited parsing step followed by independent signature verification before any claim is trusted.

Conditions for unverified parsing

Headers and claims read with ParseUnverified are not verified identity information. Both parser-construction forms below read the data without verifying its signature.

go
parser := jwt.NewParser()
parser.ParseUnverified(rawToken, jwt.MapClaims{})

parser = &jwt.Parser{}
parser.ParseUnverified(rawToken, jwt.MapClaims{})

Use this API only when verification has already succeeded or will be performed independently before the data is trusted. The verification example below uses github.com/golang-jwt/jwt/v5; check support status and API differences separately for earlier major versions or github.com/dgrijalva/jwt-go.

Potential impact

  • Forged identifiers, roles, or permissions may bypass authentication or authorization.
  • Trusting an unverified alg or kid may let an attacker influence algorithm or key selection.
  • Versions of golang-jwt/jwt/v5 earlier than 5.2.2 are affected by CVE-2025-30204, which can cause excessive memory allocation when parsing hostile tokens.

Remediation

  • Replace ParseUnverified with jwt.Parse or jwt.ParseWithClaims. Trust claims only when there is no error and the returned token is non-nil with Valid set to true.
  • Restrict signature algorithms with jwt.WithValidMethods. Obtain verification keys from trusted configuration or a validated JWKS, rather than choosing a key or algorithm solely from unverified alg or kid values.
  • Explicitly validate claims on which the application relies. In v5, options include jwt.WithExpirationRequired, jwt.WithIssuer, jwt.WithAudience, and jwt.WithAllAudiences. Use jwt.WithLeeway only with an explicit clock-skew policy.
  • Use ParseUnverified only after signature verification or when independent verification will precede all use of the claims as trusted data.
  • Use a maintained v5 release, 5.3.1 or later. Although 5.2.2 is the minimum fix for CVE-2025-30204, keep applying security updates rather than permanently pinning that minimum.

Examples

Before

go
func unverifiedClaims(r *http.Request) (jwt.MapClaims, error) {
    rawToken := r.FormValue("token")
    token, _, err := jwt.NewParser().ParseUnverified(rawToken, jwt.MapClaims{})
    if err != nil {
        return nil, err
    }
    return token.Claims.(jwt.MapClaims), nil
}

rawToken comes from the user's request, but ParseUnverified does not verify its signature. Trusting the returned claims lets an attacker supply forged values.

After

go
func verifiedToken(rawToken string, verificationKey *rsa.PublicKey) (*jwt.Token, error) {
    token, err := jwt.Parse(
        rawToken,
        func(token *jwt.Token) (any, error) {
            // Select the key from trusted issuer configuration
            // or a validated JWKS cache in production.
            return verificationKey, nil
        },
        jwt.WithValidMethods([]string{jwt.SigningMethodRS256.Alg()}),
        jwt.WithExpirationRequired(),
        jwt.WithIssuer("https://issuer.example"),
        jwt.WithAudience("api.example"),
    )
    if err != nil {
        return nil, fmt.Errorf("JWT verification failed: %w", err)
    }
    if token == nil || !token.Valid {
        return nil, errors.New("invalid JWT")
    }
    return token, nil
}

The parser fixes the allowed algorithm and required claims, verifies the signature using a trusted public key, and returns only a valid token. Apply the same options and key policy to jwt.ParseWithClaims when using a custom claims structure.

References