id: PYSEC-2026-2745 published: "2026-07-13T15:19:06.177623Z" modified: "2026-07-13T16:05:13.975025Z" aliases: - CVE-2026-45316 - GHSA-jx2x-j75f-xq3j summary: "Open WebUI: Read-Only Users Can Toggle Note Pin Status via Incorrect Permission Check (Write via Read-Only Access)" details: "### Summary\n\nThe `POST /api/v1/notes/{id}/pin` endpoint performs a write operation (toggling the `is_pinned` field) but only checks for `read` permission. Users with read-only access to a shared note can pin/unpin it, which is a state-modifying action that should require `write` permission. All other write endpoints (update, delete, access/update) correctly check for `write` permission.\n\n### Details\n\n**Affected code: `backend/open_webui/routers/notes.py` lines 412-444**\n\n```python\n@router.post('/{id}/pin', response_model=Optional[NoteModel])\nasync def pin_note_by_id(...):\n # ...\n if user.role != 'admin' and (\n user.id != note.user_id\n and not await AccessGrants.has_access(\n user_id=user.id,\n resource_type='note',\n resource_id=note.id,\n permission='read', # BUG: should be 'write'\n db=db,\n )\n ):\n raise HTTPException(...)\n \n note = await Notes.toggle_note_pinned_by_id(id, db=db) # write operation\n```\n\n**Compare with update endpoint (correct, line 318-327):**\n```python\nasync def update_note_by_id(...):\n # ...\n and not await AccessGrants.has_access(\n permission='write', # correctly checks 'write'\n )\n```\n\n### PoC\n\n**Environment:** Open WebUI v0.9.2, default configuration with notes sharing enabled.\n\n**Setup:**\n1. UserA creates a note\n2. UserA shares note with UserB with `read` permission via `POST /api/v1/notes/{id}/access/update` with `{\"access_grants\":[{\"principal_type\":\"user\",\"principal_id\":\"USERB_ID\",\"permission\":\"read\"}]}`\n\n**Test:**\n```bash\n# Step 1: UserB reads note (READ permission) -> 200 OK, write_access: false\ncurl -s http://TARGET/api/v1/notes/$NOTE_ID \\\n -H \"Authorization: Bearer $TOKEN_B\"\n# Result: 200 OK, \"write_access\": false\n\n# Step 2: UserB updates note (WRITE operation) -> 403 Forbidden (correctly blocked)\ncurl -s -X POST http://TARGET/api/v1/notes/$NOTE_ID/update \\\n -H \"Authorization: Bearer $TOKEN_B\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"title\":\"HACKED\",\"content\":\"pwned\",\"data\":{\"type\":\"note\"}}'\n# Result: 403 Forbidden\n\n# Step 3: UserB pins note (WRITE operation, but only checks READ) -> 200 OK (BUG!)\ncurl -s -X POST http://TARGET/api/v1/notes/$NOTE_ID/pin \\\n -H \"Authorization: Bearer $TOKEN_B\"\n# Result: 200 OK, \"is_pinned\": true\n\n# Step 4: UserB can toggle pin repeatedly\ncurl -s -X POST http://TARGET/api/v1/notes/$NOTE_ID/pin \\\n -H \"Authorization: Bearer $TOKEN_B\"\n# Result: 200 OK, \"is_pinned\": false (toggled back)\n```\n\n**E2E Verified Result:**\n- Step 1: UserB reads note (READ) -> 200 OK ✓\n- Step 2: UserB updates note (WRITE) -> 403 Forbidden ✓ (correctly blocked)\n- Step 3: UserB pins note (WRITE via READ) -> 200 OK, is_pinned: true ✗ (BUG)\n- Step 4: UserB toggles pin again -> 200 OK, is_pinned: false ✗ (repeated write)\n\n### Impact\n\n- A user with only `read` access to a shared note can toggle its `is_pinned` status\n- This modifies the note's state without write authorization\n- The pin status change is visible to the note owner and all other users with access\n- Privilege escalation from read to write on the pin operation\n\n**Limitations:** Only affects the `is_pinned` boolean field. Cannot modify title, content, or access_grants. Requires at least read access via explicit sharing.\n\n### Fix\n\nOne-line fix — change `permission='read'` to `permission='write'` in `pin_note_by_id`:\n\n```python\n# backend/open_webui/routers/notes.py, line 437\n- permission='read',\n+ permission='write',\n```\n\nThis makes the pin endpoint consistent with update and delete endpoints." affected: - package: name: open-webui ecosystem: PyPI purl: pkg:pypi/open-webui ranges: - type: ECOSYSTEM events: - introduced: "0" - fixed: 0.9.3 versions: - 0.1.124 - 0.1.125 - 0.2.0 - 0.2.1 - 0.2.2 - 0.2.3 - 0.2.4 - 0.2.5 - 0.3.0 - 0.3.1 - 0.3.10 - 0.3.12 - 0.3.13 - 0.3.14 - 0.3.15 - 0.3.16 - 0.3.17 - 0.3.17.dev2 - 0.3.17.dev3 - 0.3.17.dev4 - 0.3.17.dev5 - 0.3.18 - 0.3.19 - 0.3.2 - 0.3.20 - 0.3.21 - 0.3.22 - 0.3.23 - 0.3.24 - 0.3.25 - 0.3.26 - 0.3.27 - 0.3.27.dev1 - 0.3.27.dev2 - 0.3.27.dev3 - 0.3.28 - 0.3.29 - 0.3.3 - 0.3.30 - 0.3.30.dev1 - 0.3.30.dev2 - 0.3.31 - 0.3.31.dev1 - 0.3.32 - 0.3.33 - 0.3.33.dev1 - 0.3.34 - 0.3.35 - 0.3.4 - 0.3.5 - 0.3.6 - 0.3.7 - 0.3.8 - 0.3.9 - 0.4.0 - 0.4.0.dev1 - 0.4.0.dev2 - 0.4.1 - 0.4.2 - 0.4.3 - 0.4.4 - 0.4.5 - 0.4.6 - 0.4.6.dev1 - 0.4.7 - 0.4.8 - 0.5.0 - 0.5.0.dev1 - 0.5.0.dev2 - 0.5.1 - 0.5.10 - 0.5.11 - 0.5.12 - 0.5.13 - 0.5.14 - 0.5.15 - 0.5.16 - 0.5.17 - 0.5.18 - 0.5.19 - 0.5.2 - 0.5.20 - 0.5.3 - 0.5.3.dev1 - 0.5.4 - 0.5.5 - 0.5.6 - 0.5.7 - 0.5.8 - 0.5.9 - 0.6.0 - 0.6.1 - 0.6.10 - 0.6.11 - 0.6.12 - 0.6.13 - 0.6.14 - 0.6.15 - 0.6.16 - 0.6.18 - 0.6.19 - 0.6.2 - 0.6.20 - 0.6.21 - 0.6.22 - 0.6.23 - 0.6.24 - 0.6.25 - 0.6.26 - 0.6.26.dev1 - 0.6.27 - 0.6.28 - 0.6.29 - 0.6.3 - 0.6.30 - 0.6.31 - 0.6.32 - 0.6.33 - 0.6.34 - 0.6.35 - 0.6.36 - 0.6.37 - 0.6.38 - 0.6.39 - 0.6.4 - 0.6.40 - 0.6.41 - 0.6.42 - 0.6.43 - 0.6.5 - 0.6.6 - 0.6.6.dev1 - 0.6.7 - 0.6.8 - 0.6.9 - 0.7.0 - 0.7.1 - 0.7.2 - 0.8.0 - 0.8.1 - 0.8.10 - 0.8.11 - 0.8.12 - 0.8.2 - 0.8.3 - 0.8.4 - 0.8.5 - 0.8.6 - 0.8.7 - 0.8.8 - 0.8.9 - 0.9.0 - 0.9.1 - 0.9.2 references: - type: WEB url: https://github.com/open-webui/open-webui/security/advisories/GHSA-jx2x-j75f-xq3j - type: ADVISORY url: https://nvd.nist.gov/vuln/detail/CVE-2026-45316 - type: PACKAGE url: https://github.com/open-webui/open-webui - type: WEB url: https://github.com/open-webui/open-webui/releases/tag/v0.9.3 - type: PACKAGE url: https://pypi.org/project/open-webui - type: ADVISORY url: https://github.com/advisories/GHSA-jx2x-j75f-xq3j severity: - type: CVSS_V3 score: CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:N/I:L/A:N