# GO-JWT-002: JWT Parsed Without Signature Verification

> **Severity:** HIGH | **CWE:** CWE-345, CWE-347 | **OWASP:** A08:2021, A07:2021

- **Language:** Go
- **Category:** Security
- **URL:** https://codepathfinder.dev/registry/golang/security/GO-JWT-002
- **Detection:** `pathfinder scan --ruleset golang/GO-JWT-002 --project .`

## Description

JWT (JSON Web Token) security depends entirely on signature verification — the
signature proves the token was issued by a party holding the signing key and has
not been tampered with. `jwt.ParseUnverified()` in the golang-jwt/jwt library parses
the token and decodes its claims but **explicitly skips all signature validation**.

A JWT consists of three base64url-encoded sections: header.payload.signature. Any
client can construct a JWT with arbitrary claims in the payload (role: "admin",
sub: "other_user", etc.) and sign it with any key or omit the signature entirely.
Without signature verification, the server has no way to distinguish forged tokens
from legitimately issued ones.

**CVE-2020-26160** (dgrijalva/jwt-go, the predecessor to golang-jwt/jwt): This CVE
exposed another signature verification bypass — the `aud` (audience) claim was not
validated when not explicitly required, allowing tokens intended for one service to
be accepted by another. This demonstrates how JWT validation failures require attention
to the complete set of standard claims, not just the signature.

**CVE-2024-51744** (golang-jwt/jwt v5 < 5.2.2): Improper handling of newlines in
multi-line RSA/ECDSA key blocks allowed crafted keys to bypass signature verification
in certain configurations. Fixed in v5.2.2.

**RFC 8725 — "JSON Web Token Best Current Practices"** (published 2020) explicitly
recommends:
- Always perform algorithm validation against an allowlist
- "Reject tokens with `\"alg\": \"none\"`"
- Validate all claims (iss, sub, aud, exp, nbf, iat)
- Use only one key per algorithm family

**When ParseUnverified() is legitimate**: Extracting the issuer (`iss`) claim from
an unverified token to look up the correct validation key (JWKS endpoint selection).
The token MUST then be re-validated using jwt.Parse() with the retrieved key.


## Vulnerable Code

```python
# --- file: vulnerable.go ---
// GO-JWT-002 positive test cases — all SHOULD be detected
package main

import (
	"net/http"

	jwt "github.com/golang-jwt/jwt/v5"
)

func jwtParseUnverified(r *http.Request) {
	tokenStr := r.Header.Get("Authorization")
	parser := jwt.NewParser()
	// SINK: ParseUnverified skips signature check — forged tokens accepted
	token, _, _ := parser.ParseUnverified(tokenStr, &jwt.MapClaims{})
	_ = token
}

func jwtParseUnverifiedRaw() {
	tokenStr := "eyJhbGciOiJub25lIn0.eyJhZG1pbiI6dHJ1ZX0."
	var claims jwt.MapClaims
	parser := jwt.NewParser()
	// SINK: signature not checked at all
	tok, _, _ := parser.ParseUnverified(tokenStr, &claims)
	_ = tok
}

# --- file: go.mod ---
module example.com/go-jwt-002/positive

go 1.21

require github.com/golang-jwt/jwt/v5 v5.3.1

# --- file: go.sum ---
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
```

## Secure Code

```python
// SECURE: jwt.Parse validates signature AND algorithm
import (
    "fmt"
    "os"
    "github.com/golang-jwt/jwt/v5"
)

type Claims struct {
    UserID string `json:"sub"`
    Role   string `json:"role"`
    jwt.RegisteredClaims
}

func parseToken(tokenString string) (*Claims, error) {
    secret := []byte(os.Getenv("JWT_SECRET"))

    token, err := jwt.ParseWithClaims(tokenString, &Claims{},
        func(t *jwt.Token) (interface{}, error) {
            // CRITICAL: verify algorithm matches expected type
            // Prevents algorithm substitution attacks (RS256 key used with HS256)
            if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
                return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
            }
            return secret, nil
        },
        // Validate standard claims
        jwt.WithAudience("my-service"),
        jwt.WithIssuer("https://auth.example.com"),
        jwt.WithIssuedAt(),
        jwt.WithExpirationRequired(),
    )
    if err != nil {
        return nil, fmt.Errorf("invalid token: %w", err)
    }

    claims, ok := token.Claims.(*Claims)
    if !ok || !token.Valid {
        return nil, fmt.Errorf("invalid claims")
    }
    return claims, nil
}

// LEGITIMATE USE of ParseUnverified: extract issuer to look up JWKS endpoint
func lookupJWKS(tokenString string) (string, error) {
    // ParseUnverified is safe ONLY for pre-validation issuer extraction
    token, _, _ := new(jwt.Parser).ParseUnverified(tokenString, jwt.MapClaims{})
    issuer, _ := token.Claims.(jwt.MapClaims)["iss"].(string)
    // MUST then validate with jwt.Parse using the JWKS from the issuer
    return fetchJWKS(issuer) // validate token fully after this
}

```

## Detection Rule (Python SDK)

```python
"""GO-JWT-002: JWT token parsed without signature verification (ParseUnverified)."""

from codepathfinder.go_rule import QueryType
from codepathfinder import flows
from codepathfinder.go_decorators import go_rule


class GoJWTLib(QueryType):
    fqns = ["github.com/golang-jwt/jwt/v5", "github.com/golang-jwt/jwt", "github.com/dgrijalva/jwt-go"]
    patterns = ["jwt.*"]
    match_subclasses = False


@go_rule(
    id="GO-JWT-002",
    severity="HIGH",
    cwe="CWE-345",
    owasp="A08:2021",
    tags="go,security,jwt,parse-unverified,integrity,CWE-345,OWASP-A08",
    message=(
        "Detected use of jwt.ParseUnverified() which skips signature verification. "
        "Tokens parsed with ParseUnverified are not validated — an attacker can forge "
        "arbitrary claims by crafting a JWT without a valid signature. "
        "Use jwt.Parse() with a key function that validates the signing algorithm and secret."
    ),
)
def detect_jwt_parse_unverified():
    """Detect use of ParseUnverified() that skips JWT signature validation."""
    return GoJWTLib.method("ParseUnverified")
```

## How to Fix

- Use jwt.Parse() or jwt.ParseWithClaims() with an explicit key validation function.
- In the key validation function, check t.Method against the expected signing method type.
- Validate all standard claims: iss, aud, exp, nbf, iat using jwt.With* options.
- Explicitly reject tokens with alg=none in the key validation function.
- Keep golang-jwt/jwt updated — CVE-2024-51744 was fixed in v5.2.2.
- Store JWT secrets in environment variables or secrets managers, never in source code.
- Set short token expiration times (15 minutes for access tokens) to limit window of forged tokens.

## Security Implications

- **Complete Authentication Bypass:** An attacker creates a JWT with admin claims (`"role":"admin"`, `"is_admin":true`,
`"sub":"admin_user_id"`) and any signature (or no signature). ParseUnverified()
accepts this token, granting the attacker full administrative access.

- **Horizontal Privilege Escalation:** Changing the `sub` (subject) claim to another user's ID impersonates that user.
Without signature verification, every authenticated account is accessible by
any other authenticated user.

- **JWT Algorithm Confusion:** If the server accepts `"alg": "none"` tokens (a related vulnerability), the
attacker removes the signature entirely. Combining ParseUnverified with algorithm
confusion creates a completely bypassed authentication system.


## References

- [CWE-345: Insufficient Verification of Data Authenticity — MITRE](https://cwe.mitre.org/data/definitions/345.html)
- [CWE-347: Improper Verification of Cryptographic Signature — MITRE](https://cwe.mitre.org/data/definitions/347.html)
- [RFC 8725 — JSON Web Token Best Current Practices (IETF)](https://www.rfc-editor.org/rfc/rfc8725)
- [RFC 7519 — JSON Web Token (JWT) specification](https://www.rfc-editor.org/rfc/rfc7519)
- [golang-jwt/jwt library documentation (GitHub)](https://github.com/golang-jwt/jwt)
- [golang-jwt/jwt v5 documentation (pkg.go.dev)](https://pkg.go.dev/github.com/golang-jwt/jwt/v5)
- [CVE-2020-26160 — dgrijalva/jwt-go audience validation bypass](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-26160)
- [CVE-2024-51744 — golang-jwt/jwt v5 signature bypass (GitHub Advisory)](https://github.com/golang-jwt/jwt/security/advisories/GHSA-29wx-vh33-7x7r)
- [OWASP JWT Security Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_for_Java_Cheat_Sheet.html)

---

Source: https://codepathfinder.dev/registry/golang/security/GO-JWT-002
Code Pathfinder — Open source, type-aware SAST with cross-file dataflow analysis
