PYSEC-2026-3882

See a problem?
Import Source
https://github.com/pypa/advisory-database/blob/main/vulns/piccolo-admin/PYSEC-2026-3882.yaml
JSON Data
https://api.osv.dev/v1/vulns/PYSEC-2026-3882
Aliases
Published
2026-09-10T09:44:58Z
Modified
2026-09-10T12:15:10Z
Severity
  • 8.8 (High) CVSS_V3 - CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H CVSS Calculator
Summary
piccolo-admin has a privilege escalation issue - admin to superuser via session-token disclosure in GET /api/tables/sessions/.
Details

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

  1. Network reachability to the admin.
  2. Valid credentials for a non-superuser admin (admin=True, superuser=False — the default role created by BaseUser.create_user(admin=True)).
  3. The deployment includes the Sessions table (and typically the User table) in create_admin([...]) — 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.) 01-john-piccolo_user-list

  2. 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. 02-john-save-blocked-405 03-john-save-blocked-405

  3. 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.) 04-john-sees-all-session-tokens

  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.) 05-john-self-promote

  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 HIGHCVSS: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_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.
  • The shipped 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.
  • I'm happy to coordinate disclosure timing and validate any candidate patch.
References

Affected packages

PyPI / piccolo-admin

Package

Name
piccolo-admin
View open source insights on deps.dev
Purl
pkg:pypi/piccolo-admin

Affected ranges

Type
ECOSYSTEM
Events
Introduced
0 Unknown introduced version / All previous versions are affected
Fixed
1.14.0

Affected versions

0.*
0.1.0
0.1.1
0.1.2
0.1.3
0.1.4
0.2.0
0.3.0
0.3.1
0.3.2
0.3.3
0.3.4
0.3.5
0.3.6
0.3.7
0.3.8
0.4.0
0.4.1
0.5.0
0.5.1
0.6.0
0.6.1
0.6.2
0.6.3
0.6.4
0.6.5
0.6.6
0.7.0
0.8.0
0.8.1
0.9.0
0.9.1
0.9.2
0.10.0
0.10.1
0.10.2
0.10.3
0.10.4
0.10.5
0.10.6
0.10.7
0.10.8
0.10.9
0.11.0
0.11.1
0.11.2
0.11.3
0.11.4
0.11.5
0.11.6
0.11.7
0.11.8
0.11.9
0.11.10
0.11.11
0.11.12
0.11.13
0.12.0
0.12.1
0.13.0
0.13.1
0.13.2
0.14.0
0.15.0
0.15.1
0.15.2
0.16.0
0.16.1
0.17.0
0.18.0
0.18.1
0.18.2
0.19.0
0.19.1
0.19.2
0.19.3
0.19.4
0.19.5
0.19.6
0.20.0
0.21.0
0.22.0
0.22.1
0.22.2
0.23.0
0.24.0
0.25.0
0.26.0
0.26.1
0.27.0
0.28.0
0.29.0
0.29.1
0.30.0
0.31.0
0.31.1
0.31.2
0.32.0
0.33.0
0.33.1
0.34.0
0.35.0
0.36.0
0.37.0
0.38.0
0.39.0
0.40.0
0.41.0
0.42.0
0.43.0
0.44.0
0.45.0
0.45.1
0.45.2
0.46.0
0.47.0
0.48.0
0.49.0
0.50.0
0.51.0
0.52.0
0.53.0
0.54.0
0.55.0
0.56.0
0.57.0
0.58.0
1.*
1.0.0
1.1.0
1.1.1
1.1.2
1.1.3
1.2.0
1.2.1
1.2.2
1.3.0
1.3.1
1.3.2
1.3.3
1.4.0
1.5.0
1.6.0
1.7.0
1.7.1
1.8.0
1.8.1
1.8.2
1.9.0
1.9.1
1.10.0
1.11.0
1.12.0
1.13.0

Database specific

source
"https://github.com/pypa/advisory-database/blob/main/vulns/piccolo-admin/PYSEC-2026-3882.yaml"