v0.14 is out: a mobile auth pack for Swift/iOS and Android, catching insecure token storage, cleartext traffic, and OAuth in embedded WebViews. Read more →
HIGH AI PREVALENCE: LOW auth.go.jwt.untrusted-verify-key

Untrusted request input flows into the verification key returned by a golang-jwt Keyfunc (or into the WithValidMethods allowlist).

Why AI tools produce this: AI coding tools rarely emit this on their own, but it still slips into assisted edits.

Why this matters

When the attacker controls the key, they sign their own forged token and supply the matching key, so every token "verifies": a complete authentication bypass. When the attacker controls the accepted methods, they can downgrade verification and defeat the signature check (CWE-347, Improper Verification of Cryptographic Signature).

The verification key and the accepted algorithms must be fixed server-side. Return the key from trusted configuration or a vetted key set keyed by a validated kid, and pin accepted methods to a constant allowlist. Never resolve them from r.URL.Query(), r.FormValue, or a request header.

VULNERABLE
vulnerable.go
package main

import (
	"net/http"

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

// The HMAC secret is taken from a request query parameter and returned as the
// verification key — the attacker signs their own token with a key they choose.
func verifyQueryKey(w http.ResponseWriter, r *http.Request) {
	key := r.URL.Query().Get("key")
	// ruleid: auth.go.jwt.untrusted-verify-key
	_, _ = jwt.Parse(r.FormValue("token"), func(t *jwt.Token) (interface{}, error) {
		return []byte(key), nil
	})
}

// The accepted signing methods come from a request header.
func verifyUntrustedMethods(w http.ResponseWriter, r *http.Request) {
	alg := r.Header.Get("X-Alg")
	// ruleid: auth.go.jwt.untrusted-verify-key
	parser := jwt.NewParser(jwt.WithValidMethods([]string{alg}))
	_, _ = parser.Parse(r.FormValue("token"), func(t *jwt.Token) (interface{}, error) {
		return []byte("server-secret"), nil
	})
}

func main() {
	http.HandleFunc("/v", verifyQueryKey)
	http.HandleFunc("/m", verifyUntrustedMethods)
	_ = http.ListenAndServe(":8080", nil)
}
SAFE
safe.go
package main

import (
	"net/http"
	"os"

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

// Safe: the verification key comes from server configuration, and the token
// (the only request-derived value) is correctly the parsed argument, not the
// key. The token being request-controlled must NOT trigger the rule.
func verifyConfigKey(w http.ResponseWriter, r *http.Request) {
	secret := []byte(os.Getenv("JWT_SECRET"))
	// ok: auth.go.jwt.untrusted-verify-key
	_, _ = jwt.Parse(r.FormValue("token"), func(t *jwt.Token) (interface{}, error) {
		return secret, nil
	})
}

// Safe: the accepted methods are a fixed server-side allowlist.
func verifyPinnedMethods(w http.ResponseWriter, r *http.Request) {
	// ok: auth.go.jwt.untrusted-verify-key
	parser := jwt.NewParser(jwt.WithValidMethods([]string{"RS256"}))
	_, _ = parser.Parse(r.FormValue("token"), func(t *jwt.Token) (interface{}, error) {
		return []byte(os.Getenv("JWT_SECRET")), nil
	})
}

func main() {
	http.HandleFunc("/c", verifyConfigKey)
	http.HandleFunc("/p", verifyPinnedMethods)
	_ = http.ListenAndServe(":8080", nil)
}

Suppressing this rule

If a finding is a genuine false positive, scope the suppression to the exact line and leave a reason, never disable the rule project-wide. Disable directives are line-scoped by design.

// oauthlint-disable-next-line auth.go.jwt.untrusted-verify-key -- <reason>

References

https://cwe.mitre.org/data/definitions/347.html ↗https://pkg.go.dev/github.com/golang-jwt/jwt/v5#Keyfunc ↗https://datatracker.ietf.org/doc/html/rfc7518#section-3.1 ↗