CVE-2026-41160
Description
EspoCRM is an open source customer relationship management application. Prior to 9.3.5, a business logic flaw (Broken Access Control) in EspoCRM 9.3.3 allows low-privileged users to pin arbitrary notes without having the required edit permissions for the parent object. Due to a "write first, authorize later" execution flaw in the backend API, even though the server correctly returns a 403 Forbidden error, the targeted note's pinned status is already persistently modified in the database. The root cause lies in the server-side processing of the POST /api/v1/Note/{id}/pin endpoint. In application/Espo/Tools/Stream/Api/PostNotePin.php, the process() method first calls getNote($id) before calling checkParent($note). This vulnerability is fixed in 9.3.5.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
EspoCRM before 9.3.5 has a broken access control flaw allowing low-privileged users to pin notes without proper permissions because authorization occurs after the database write.
Vulnerability
A broken access control vulnerability exists in EspoCRM versions prior to 9.3.5, specifically in the POST /api/v1/Note/{id}/pin endpoint. The flaw is in the server-side processing order within application/Espo/Tools/Stream/Api/PostNotePin.php: the process() method calls getNote($id) and persists the pin status before calling checkParent($note) to verify edit permissions on the parent object. This allows the database state to be altered even when the server subsequently returns a 403 Forbidden error. The issue affects EspoCRM 9.3.3 and likely earlier versions; it is fixed in version 9.3.5 [1].
Exploitation
An attacker needs a low-privileged user account (low1) with only Read access and no Edit permission on a target entity (e.g., Account). The attacker must obtain a valid Note ID from the target object's stream (e.g., by intercepting a read request). Then they send a manual POST request to the pin endpoint using the low-privileged user's authentication tokens. The server answers with a 403 Forbidden, but the note's pinned status is already persisted in the database [1].
Impact
By exploiting this vulnerability, a low-privileged attacker can successfully pin arbitrary notes belonging to objects they are not authorized to edit. The attacker gains an unauthorized state change (note pinning) without having the required edit permissions, violating the intended access control model. The impact is primarily integrity (unauthorized modification of data) and could be used to escalate privileges in workflows where pinned status affects visibility or notifications [1].
Mitigation
EspoCRM has released version 9.3.5 which fixes this vulnerability by reordering the permission check before the database write operation. Users should upgrade to version 9.3.5 or later. No workaround is provided for unpatched instances. This CVE is not currently listed on the CISA Known Exploited Vulnerabilities (KEV) catalog [1].
AI Insight generated on May 28, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
2Patches
1e31024e72843Fix note pin
1 file changed · +3 −3
application/Espo/Tools/Stream/Api/PostNotePin.php+3 −3 modified@@ -72,6 +72,9 @@ public function process(Request $request): Response $this->checkParent($note); $this->checkPinnedCount($note); + $note->setIsPinned(true); + $this->entityManager->saveEntity($note); + return ResponseComposer::json(true); } @@ -91,9 +94,6 @@ private function getNote(string $id): Note throw new Forbidden("No read access."); } - $note->setIsPinned(true); - $this->entityManager->saveEntity($note); - return $note; }
Vulnerability mechanics
Root cause
""write first, authorize later" execution order in `PostNotePin.php` — the note is pinned and saved to the database before the parent-object edit-permission check is performed."
Attack vector
A low-privileged attacker with valid authentication tokens sends a manual `POST` request to `/api/v1/Note/{id}/pin` for a note whose parent object (e.g., an Account) they have only read access to [ref_id=1]. The server processes the pin and persists it to the database before performing the authorization check, which then returns a `403 Forbidden` with `X-Status-Reason: No parent edit access.` [ref_id=1]. Despite the error response, the note's pinned status is already committed in the database, making the attack successful even though the UI hides the "Pin" button for unauthorized users [ref_id=1].
Affected code
The vulnerable endpoint is `POST /api/v1/Note/{id}/pin`, implemented in `application/Espo/Tools/Stream/Api/PostNotePin.php`. The `process()` method calls `getNote($id)` (which pins and saves the note) *before* calling `checkParent($note)` (the authorization check) [ref_id=1]. By contrast, the unpin endpoint (`DeleteNotePin.php`) correctly performs authorization before the database write [ref_id=1].
What the fix does
The patch [patch_id=2961933] moves the `checkParent($note)` authorization call to execute *before* the note is pinned and saved in the database. This ensures that if the user lacks edit permissions on the parent object, the request is rejected with a 403 error before any database write occurs, closing the "write first, authorize later" window [ref_id=1].
Preconditions
- authAttacker must have a valid authenticated session (low-privilege user) on the EspoCRM instance
- inputAttacker must know or discover the target Note ID (obtainable via the Account stream read API)
- configAttacker must have at least read access to the parent object (e.g., Account) to view the note, but no edit access
- inputThe target note must exist and be associated with a parent object that the attacker cannot edit
Reproduction
1. As an administrator, create an Account record and post a note in its stream. Create a low-privilege user (`low1`) with only Read = All access and Edit = No access to the Account entity [ref_id=1]. 2. As `low1`, retrieve the target Note ID by intercepting the read request to `GET /api/v1/Account/{id}/stream` [ref_id=1]. 3. Send a manual `POST` request to `/api/v1/Note/{noteId}/pin` using `low1`'s authentication tokens (Authorization, Espo-Authorization, Espo-Authorization-By-Token, and Cookie headers) [ref_id=1]. 4. Observe the server responds with `403 Forbidden` and `X-Status-Reason: No parent edit access.`, but refreshing the page shows the note is now pinned, confirming the unauthorized write succeeded [ref_id=1].
Generated on May 28, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
1News mentions
0No linked articles in our index yet.