Skip to content

Disabled administrator JWT can create a new administrator and regain access

High
chase-roohms published GHSA-xv67-5v66-38qv Sep 29, 2026

Package

transmute

Affected versions

<= 2.0.0

Patched versions

>= 2.1.0

Description

Summary

Disabling a Transmute administrator does not revoke all authorization available
through that administrator's existing JWT. Normal authenticated endpoints use
get_current_active_user() and correctly reject the token, but the user-creation
endpoint uses get_current_user_optional(). The optional dependency resolves a
JWT to a user without checking disabled, and the route checks only whether the
returned user's role is admin.

A disabled administrator can therefore use a still-valid JWT to create a new,
enabled administrator account, log into it, and permanently regain full
administrative access. The default JWT lifetime is 60 minutes.

API-key authentication does not have the same flaw because
_resolve_user_from_api_key() explicitly rejects disabled users.

Affected versions

  • Introduced in a4af2e7646cc0d1f21e50e2cafcf8639c185fc99
    (feat: add JWT authentication, user management, and auth infrastructure).
  • Affected: all releases v0.0.1 through v2.0.0 and current main at
    a9ff811498b5b71667ad199766a0e746ab37d508.
  • Confirmed through the real FastAPI routes in the official v2.0.0 image.

Root cause

backend/api/routes/users.py declares:

current_user: dict | None = Depends(get_current_user_optional)

For an existing installation, it then authorizes creation using only:

if current_user["role"] != "admin":
    raise HTTPException(status_code=403, ...)

In backend/api/deps.py, the JWT branch of get_current_user_optional() returns
the database user even when user["disabled"] is true. In contrast,
get_current_active_user() rejects disabled users and the API-key resolution
path also checks the flag.

Reproduction

The attached probe_disabled_admin_jwt.py performs the complete API flow in an
isolated official v2.0.0 container:

  1. Create and authenticate first-admin; retain its JWT.
  2. Create and authenticate a second administrator.
  3. Use the second administrator to set first-admin.disabled=true.
  4. Verify the retained JWT receives 403 on GET /api/users/me.
  5. Send the same JWT to POST /api/users, requesting an enabled account with
    role admin.
  6. Authenticate the newly created account.

Observed output:

DISABLED_TOKEN_NORMAL_ENDPOINT 403 {'detail': 'User account is disabled'}
DISABLED_TOKEN_CREATE_ADMIN 201 {... 'role': 'admin', 'disabled': False ...}
RECOVERY_ADMIN_LOGIN 200
RECOVERY_ROLE admin

Impact

Account disablement is an incident-response and access-revocation control. A
removed administrator with an unexpired browser JWT can bypass that control,
establish a fresh persistent administrator, manage all users, view global
statistics, change system-wide settings, and retain control after the original
JWT expires.

Severity

Suggested CVSS v3.1:

CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H — 7.2

The high-privilege prerequisite reflects that the attacker must possess a
previously issued administrator JWT. The security boundary being bypassed is
the subsequent explicit disablement/revocation of that account.

Weaknesses: CWE-863 (Incorrect Authorization) and CWE-613 (Insufficient Session
Expiration).

Recommended remediation

Reject disabled users consistently in the optional JWT path, or add an
optional-active-user dependency and use it for bootstrap-aware user creation.
The route should return 403 if a supplied valid token belongs to a disabled
user. Add a regression test proving that a disabled admin JWT cannot create any
user, while an unauthenticated request can still create only the first account
on an empty installation.

Minimal route-level defense:

if current_user is not None and current_user.get("disabled"):
    raise HTTPException(status_code=403, detail="User account is disabled")

Longer term, consider token revocation/versioning so administrative disablement
can invalidate all already-issued JWTs immediately and uniformly.

AI-assistance disclosure

AI-assisted tooling was used to help inspect code, construct the isolated API
proof, and draft this report. The reporter reviewed and reproduced the issue
against the official image and takes responsibility for the submission.

Severity

High

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
High
User interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
High
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H

CVE ID

No known CVE

Weaknesses

Insufficient Session Expiration

According to WASC, Insufficient Session Expiration is when a web site permits an attacker to reuse old session credentials or session IDs for authorization. Learn more on MITRE.

Incorrect Authorization

The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check. Learn more on MITRE.

Credits