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.py.jwt.untrusted-verify-key

Untrusted request input flows into the verification key or the algorithms allowlist of jwt.decode(...) (PyJWT / python-jose).

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 algorithms, they can downgrade verification (e.g. to HS256 against a public key, or to none on older libraries) and defeat the signature check (CWE-347, Improper Verification of Cryptographic Signature).

The verification key and the accepted algorithms must be fixed server-side. Pin algorithms to a constant allowlist (algorithms=["RS256"]) and resolve the key from trusted configuration or a vetted key set keyed by a validated kid, never from request.args, request.form, request.json, or request.headers.

VULNERABLE
vulnerable.py
"""Request-controlled key / algorithms flowing into jwt.decode (PyJWT)."""

import jwt
from flask import Flask, request

app = Flask(__name__)

PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"


@app.route("/a")
def attacker_key():
    token = request.headers.get("Authorization")
    key = request.args.get("key")
    # ruleid: auth.py.jwt.untrusted-verify-key
    return jwt.decode(token, key, algorithms=["HS256"])


@app.route("/b")
def attacker_algorithms():
    token = request.headers.get("Authorization")
    algs = request.args.getlist("alg")
    # ruleid: auth.py.jwt.untrusted-verify-key
    return jwt.decode(token, PUBLIC_KEY, algorithms=algs)


@app.route("/c")
def attacker_key_kwarg():
    token = request.args.get("token")
    header_key = request.headers.get("X-Key")
    # ruleid: auth.py.jwt.untrusted-verify-key
    return jwt.decode(token, key=header_key, algorithms=["HS256"])
SAFE
safe.py
"""Server-pinned key and algorithms — the token may come from the request."""

import jwt
from flask import Flask, request

app = Flask(__name__)

PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"
ALLOWED_ALGS = ["RS256"]


@app.route("/a")
def pinned():
    # ok: auth.py.jwt.untrusted-verify-key -- key and algorithms are server constants
    token = request.headers.get("Authorization")
    return jwt.decode(token, PUBLIC_KEY, algorithms=ALLOWED_ALGS)


@app.route("/b")
def token_from_request_is_fine():
    # ok: auth.py.jwt.untrusted-verify-key -- only the token is request-controlled (expected); key/algs are fixed
    token = request.args.get("access_token")
    return jwt.decode(token, PUBLIC_KEY, algorithms=["RS256"])


@app.route("/c")
def validated_algorithm():
    # ok: auth.py.jwt.untrusted-verify-key -- candidate algorithm vetted against an allowlist before use
    token = request.headers.get("Authorization")
    alg = request.args.get("alg")
    return jwt.decode(token, PUBLIC_KEY, algorithms=[validate_algorithm(alg)])


def validate_algorithm(candidate: str) -> str:
    if candidate not in {"RS256", "ES256"}:
        raise ValueError("unsupported algorithm")
    return candidate

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.py.jwt.untrusted-verify-key -- <reason>

References

https://cwe.mitre.org/data/definitions/347.html ↗https://datatracker.ietf.org/doc/html/rfc7518#section-3.1 ↗https://owasp.org/www-project-api-security/ ↗