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:
- Create and authenticate
first-admin; retain its JWT.
- Create and authenticate a second administrator.
- Use the second administrator to set
first-admin.disabled=true.
- Verify the retained JWT receives 403 on
GET /api/users/me.
- Send the same JWT to
POST /api/users, requesting an enabled account with
role admin.
- 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.
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-creationendpoint uses
get_current_user_optional(). The optional dependency resolves aJWT to a user without checking
disabled, and the route checks only whether thereturned 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
a4af2e7646cc0d1f21e50e2cafcf8639c185fc99(
feat: add JWT authentication, user management, and auth infrastructure).a9ff811498b5b71667ad199766a0e746ab37d508.Root cause
backend/api/routes/users.pydeclares:For an existing installation, it then authorizes creation using only:
In
backend/api/deps.py, the JWT branch ofget_current_user_optional()returnsthe database user even when
user["disabled"]is true. In contrast,get_current_active_user()rejects disabled users and the API-key resolutionpath also checks the flag.
Reproduction
The attached
probe_disabled_admin_jwt.pyperforms the complete API flow in anisolated official v2.0.0 container:
first-admin; retain its JWT.first-admin.disabled=true.GET /api/users/me.POST /api/users, requesting an enabled account withrole
admin.Observed output:
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.2The 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:
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.