Piccolo-Admin superuser escalation via session-token disclosure on read-only CRUD routes — operator validation¶
Date reviewed: 2026-08-30 Advisory: GHSA-2gh4-jmwq-rr8w — high, CVSS 8.8 Affected: piccolo-admin <= 1.13.0 (pip) and piccolo-api <= 1.9.x line; fixed in piccolo-admin 1.14.0 / piccolo-api 1.10.0 Boundary class: role-gated CRUD routes where the method allowlist gates write verbs but not reads, and the read response includes a live credential column
The primitive¶
piccolo_admin gates the user and session tables behind a superuser_validators helper for non-superuser admins. The helper rejects PUT, PATCH, DELETE, and POST — but does not reject GET. Two properties combine into the escalation:
- The
sessionstable stores live session tokens in plaintext, and the token column is not markedsecret=True, so the token value is included in everyGETresponse. - Any non-superuser admin (a realistic, documented role) can therefore list every other user's live session token with one request:
GET /api/tables/sessions/(andGET /api/tables/users/for the target mapping).
The chain:
- Non-superuser admin enumerates sessions via the read-only CRUD route.
- Replay a target user's token as
Cookie: id=<token>(or the app's auth header) to impersonate that user — including the superuser. - With superuser access, set
superuser = trueon the attacker's own row (self-promote), making the privilege permanent.
Reachable on the documented deployment pattern: a deployer adds the Sessions and User tables to create_admin([...]) so superusers get a UI to monitor and revoke sessions — the feature that makes the tables visible is what makes the tokens readable.
Why this is durable¶
The reusable class is role-gating that lists allowed write verbs but forgets the read verb, on a table whose rows contain live credentials. Concrete heuristics:
- Method-allowlist role gates. Any validator/middleware that blocks
POST/PUT/PATCH/DELETEfor a role: checkGET,HEAD, OPTIONS, and any custom verbs. The gate is only as strong as the verb list. - Credential-adjacent tables exposed to admin UIs. Session tables, API-key tables, refresh-token stores, and MFA/2FA token tables frequently get added to admin panels for "monitoring and revocation." Check whether the read path strips or masks secret columns, or whether the ORM layer marks them
secret/excluded-from-serialization. - Session-token replay semantics. If the token format is a bare cookie value with no binding (no device fingerprint, no IP pin, no single-use), read-only disclosure of the token column is full account takeover. Confirm the replay works with a synthetic target session in the lab.
- Self-promotion sink. After impersonation, check whether the app allows a user to edit their own row for privilege fields.
superuser = trueon the self-row is the permanent-escalation primitive.
Replayable validation (authorized lab only)¶
Preconditions: a lab deployment of piccolo-admin <= 1.13.0 (or equivalent role-gated CRUD setup) with a superuser, a non-superuser admin, and the User/Sessions tables registered in the admin. Disposable accounts only; no production database; no real user sessions.
- Recon the CRUD surface. As the non-superuser admin, enumerate the registered admin tables. Confirm
usersandsessionsare present. - Method-matrix the gate. Against
GET,POST,PUT,PATCH,DELETEon/api/tables/sessions/(and/api/tables/users/), record the status per verb. Positive:GETreturns 200 with token values in the body while write verbs return 403. - Token disclosure proof. Capture the shape of a session row (redact the real token). Verify the token column is present and unmasked in the JSON.
- Replay proof. Create a lab-only target user, log in through the UI to mint a session, fetch its token from the disclosure step, and replay it in a fresh client. Positive: the session authenticates as the target user without re-login.
- Self-promotion proof. With the replayed superuser session, write
superuser = trueon the attacker's lab row via the admin UI/API. Positive: the attacker's role is now superuser. - Negative control. Repeat the matrix on
piccolo-admin 1.14.0+ /piccolo-api 1.10.0+ and confirmGETon the gated tables is denied.
Stop at "read route returns the live token and the token replays." Do not exfiltrate real tokens, do not target production users, and do not persist real account changes outside the lab.
Reporting heuristic¶
- Lead with the method-allowlist gap → credential disclosure → replay → self-promotion chain. Name the exact validator (
superuser_validators), the exact route, the exact column, and the replay cookie/header. - Include versions of
piccolo-adminandpiccolo-api, the admin-table registration, the verb matrix, and a redacted sample of the token-bearing response. - Distinguish the read-gate gap (the finding) from the replay impact (the proof). Do not claim persistence without the self-promotion step.
- Note the configuration precondition: the tables must be registered in
create_admin([...]). If they are not registered, the chain does not exist — say so in the report.
Safe boundaries¶
- Authorized lab deployment only; disposable accounts; no production database.
- Redact all session tokens and PII in evidence. Do not publish real tokens.
- No impersonation of real users; replay proofs use lab-minted sessions.
- No account changes outside the lab; self-promotion proof is reverted after capture.