home / findings / wm-2026-004

Python FastAPI: JWT signature is never verified, so any token is accepted

ID
WM-2026-004
Severity
Critical  CVSS 3.1 base 9.1
Vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N
Class
Improper authentication · CWE-347 Improper Verification of Cryptographic Signature, CWE-287 Improper Authentication
Component
api.plankdesigns.co, Python 3.12 / FastAPI, get_current_user dependency
Status
Fixed, published after the fix
Published
2026-10-04

Executive summary

The authentication dependency decodes the bearer token with signature verification switched off and then trusts the claims inside it, including the user id and role. An attacker can create a token with any contents, using any key, and be treated as any user, including an administrator.

Impact

  • Full authentication bypass: impersonation of any user without knowing any secret.
  • Access to administrative endpoints by claiming role: admin, including data export.
  • All data exposed through the API is readable and writable as the impersonated user.

Technical details

root cause

Authentication is implemented as a FastAPI dependency used by every protected route. It decodes the JWT, but disables signature verification, probably left over from debugging:

# auth.py (vulnerable)
import jwt
from fastapi import Depends, HTTPException
from fastapi.security import HTTPBearer

bearer = HTTPBearer()

def get_current_user(cred = Depends(bearer)):
    try:
        claims = jwt.decode(cred.credentials, options={"verify_signature": False})
    except jwt.PyJWTError:
        raise HTTPException(401)
    return User(id=claims["sub"], role=claims["role"])   # role taken from the token

A JWT is only meaningful if its signature is checked. With verification disabled, the token is just attacker-supplied JSON, and the server then takes both the identity (sub) and the privilege (role) from it. The expiry is not checked either, so a forged token never expires.

Proof of concept

minimal, no real data

Reproduced against a local copy of the API, using an arbitrary key that the server has never seen.

$ python3 -c 'import jwt; print(jwt.encode({"sub":"1","role":"admin"}, "any-key", algorithm="HS256"))'
<token>

$ curl -s https://api.plankdesigns.co/api/v1/admin/export -H "Authorization: Bearer <token>"
HTTP/1.1 200 OK       # accepted; no valid credential was needed

The export response was not read; the status code alone confirmed the bypass.

Remediation

Verify the signature with the server's key, pin the allowed algorithm, require the expiry, and read the role from the database instead of the token.

# auth.py (fixed)
def get_current_user(cred = Depends(bearer), db = Depends(get_db)):
    try:
        claims = jwt.decode(
            cred.credentials,
            settings.JWT_SECRET,                 # from a secret store, not source code
            algorithms=["HS256"],                # pin; never accept "none"
            audience="api.plankdesigns.co",
            options={"require": ["exp", "sub", "aud"]},
        )
    except jwt.PyJWTError:
        raise HTTPException(401)
    user = db.get(User, claims["sub"])           # current role comes from the database
    if user is None or user.disabled:
        raise HTTPException(401)
    return user
  • Rotate the signing secret and invalidate existing sessions, because forged tokens may already have been issued.
  • Prefer asymmetric signing (for example RS256 or EdDSA) so services that verify tokens do not hold the signing key.
  • Add a test that sends a token signed with the wrong key and expects 401.
  • Search for other uses of verify_signature set to false, and for other places that trust claims without verification.

Detection

Alert on successful requests whose token was not issued by the identity service, for example by logging the token id (jti) and comparing it with issued tokens.

Disclosure timeline

days from report
d0
Report sent to the vendor security contact
d1
Acknowledged
d3
Fix deployed and all existing sessions invalidated
d10
Retested; access log review completed
d90
Public disclosure window (published after the fix)

References

All findings

Tags

#web#api#jwt#authentication

Related

shares a tag