Share
## https://sploitus.com/exploit?id=0A738D4C-E642-58D3-988B-4E964946EC66
# CVE-2026-34213
A low-privileged Docmost user could supply a victim attachmentId to the generic upload endpoint and overwrite another page's stored attachment inside the same workspace.

## Intro

I identified, responsibly disclosed, and reproduced a **High-severity** authorization flaw in **Docmost**, the open-source collaborative documentation platform.

Docmost’s official site presents it as an enterprise-ready on-premises wiki with **3M+ downloads**, and says it is trusted by teams at organizations including **Vilnius City**, **Bechtle**, the **Australian Government**, the **Red Cross**, and **ETS Quebec**.

The bug lived in the generic file-upload path that Docmost also uses for diagram save/update flows.

I was reviewing that code with a very specific question in mind:

**What happens if the upload endpoint proves edit access on one page, but the overwrite target is selected with a separate user-controlled attachment ID?**

In this case, that question led directly to a real object-binding failure.

Docmost allowed a caller to send:

- a `pageId` for a page they were allowed to edit, and
- an `attachmentId` belonging to a different page in the same workspace

The server did perform an overwrite consistency check, but the guard used the wrong boolean logic.

That meant the request could pass authorization and overwrite the victim attachment anyway.

This issue became **CVE-2026-34213**.

**Docmost:** [docmost/docmost](https://github.com/docmost/docmost)  
**Advisory:** [GHSA-89fp-2hch-j9gp](https://github.com/docmost/docmost/security/advisories/GHSA-89fp-2hch-j9gp)  
**CVE:** CVE-2026-34213  
**Patched in:** `v0.71.0`


---

## Attack Chain

`attacker-controlled pageId with edit access -> attacker-controlled victim attachmentId -> flawed overwrite guard treats cross-page overwrite as valid -> storage path rebuilt from victim attachmentId -> attacker bytes replace victim file -> victim page continues serving modified attachment`

---

## What This Part of Docmost Does

Docmost stores uploaded page attachments as database records plus backing files in storage.

For normal uploads, the server creates a fresh attachment ID and writes a new file.

For diagram save/update flows, however, the client intentionally reuses an existing `attachmentId` so the same diagram file can be updated in place instead of generating a brand-new attachment record every time.

That behavior is legitimate on its own.

The problem is that it creates a high-risk path:

- one input identifies the page being authorized
- another input identifies the attachment being overwritten

Whenever an endpoint mixes those two responsibilities, the implementation must bind them together exactly.

Docmost did not.

---

## Why This Surface Was Worth Looking At

Mixed create/update endpoints are common places for authorization bugs.

The reason is simple:

- create flows are usually authorized against the container object
- update flows are usually authorized against the existing record
- if one endpoint tries to do both, it is easy to validate the wrong thing first and treat the second identifier as "just metadata"

That is exactly the pattern here.

`POST /api/files/upload` validated that the caller could edit the page named by `pageId`.

But if `attachmentId` was also supplied, the server switched into an overwrite path and selected an existing attachment record separately.

That made the critical security question:

**does the overwrite path prove that the selected attachment actually belongs to the authorized page?**

The answer in vulnerable versions was no.

---

## Root Cause

The root cause was an **authorization bypass through a user-controlled key**, combined with a boolean logic bug in the overwrite guard.

The vulnerable flow looked like this:

1. `AttachmentController.uploadFile()` read `pageId` from multipart form data.
2. It loaded that page and called `validateCanEdit(page, user)`.
3. It separately accepted an optional `attachmentId` from the same request.
4. `AttachmentService.uploadFile()` loaded the existing attachment by that attacker-supplied ID.
5. The overwrite guard attempted to verify the existing attachment matched the authorized page.
6. The guard used `&&` instead of rejecting on any mismatch.

The vulnerable guard was:

```ts
if (
  existingAttachment.pageId !== pageId &&
  existingAttachment.fileExt !== preparedFile.fileExtension &&
  existingAttachment.workspaceId !== workspaceId
) {
  throw new BadRequestException("File attachment does not match");
}
```

That condition only rejected the request if:

- the page ID mismatched, and
- the file extension mismatched, and
- the workspace ID mismatched

all at the same time.

That is the opposite of what an overwrite guard should do.

For the real attack case, the attacker intentionally stayed inside the same workspace.

So:

- `existingAttachment.workspaceId !== workspaceId` was `false`

Once that operand became false, the whole `&&` condition evaluated to false, even if the attachment belonged to a different page.

So the server treated a cross-page overwrite as valid.

That was the first half of the bug.

The second half is what made the impact real.

After the check, the service rebuilt the destination storage path using the attacker-supplied `attachmentId` and filename:

```ts
const filePath =
  `${getAttachmentFolderPath(AttachmentType.File, workspaceId)}/` +
  `${attachmentId}/${preparedFile.fileName}`;
```

Then, on the update path, Docmost only updated mutable metadata such as:

- `fileSize`
- `updatedAt`

It did **not** rebind ownership to the attacker page.

So the victim page kept pointing at the same attachment record and the same attachment ID.
Only the underlying file bytes changed.

That is why this was not a harmless mismatch.

It was a persistent unauthorized overwrite primitive.

---

## Why This Is a Security Issue, Not Just a Logic Mistake

This was not a cosmetic bug and not a filename collision issue.

The attacker did not need a race.
The attacker did not need to guess a random path.
The attacker did not need write access to the victim page.

They only needed:

- read access to learn a victim attachment reference, and
- write access to any other page in the same workspace

From there, they could replace the stored file bytes for another page's attachment while the victim page continued to reference and serve that attachment as if nothing had changed.

That is a direct integrity failure.

In practical terms, the attacker could:

- tamper with diagrams
- replace attachments with misleading content
- corrupt referenced files
- create confusing audit trails because the attachment still appeared to belong to the victim page

The important point is this:

**the server accepted an overwrite target chosen by the attacker, without binding it to the page whose edit permission had actually been checked.**

That is an access-control failure, not just bad boolean hygiene.

---

## Why Exploitation Was Practical

The exploit was especially practical for diagram attachments.

Docmost's client intentionally reuses `attachmentId` for diagram saves and uses deterministic filenames:

- `diagram.excalidraw.svg`
- `diagram.drawio.svg`

That matters because it lowers the attacker's requirements.

For generic attachments, the attacker needs both:

- the victim attachment ID
- the victim filename

For diagrams, the filename is already predictable.

So if the attacker can read the victim page content, they can often recover the only missing piece they need:

- the victim `attachmentId`

In my validation setup, I used exactly that path:

- the attacker had only **reader** access to the victim space
- the attacker had **writer** access to a different attacker-controlled space
- both spaces belonged to the same workspace

That was enough.

The exploit crossed page boundaries and crossed space boundaries inside the same workspace, while still satisfying the flawed workspace check.

---

## Proof of Concept

I validated the issue live against **Docmost `v0.70.3`** using a disposable lab built from `docmost/docmost:0.70.3`, Postgres, and Redis.

The PoC flow was:

1. Create an owner account.
2. Create a victim space and an attacker-controlled space in the same workspace.
3. Invite a second user as the attacker.
4. Grant the attacker:
   - reader access to the victim space
   - writer access to the attacker-controlled space
5. In the victim space, upload a diagram attachment to a victim page.
6. As the attacker, retrieve the victim page information and note the victim `attachmentId`.
7. Send `POST /api/files/upload` with:
   - `pageId` = attacker page ID
   - `attachmentId` = victim attachment ID
   - `file` = attacker-controlled replacement file using the victim filename
8. Download the victim attachment before and after the overwrite and compare hashes.

The minimal request shape was:

```http
POST /api/files/upload
Content-Type: multipart/form-data

pageId=
attachmentId=
file=@diagram.excalidraw.svg;filename=diagram.excalidraw.svg
```

The observed live result was:

- victim attachment ID learned by attacker: `019d18ae-b176-751c-8525-b5f3cede131d`
- attacker page ID used for the overwrite request: `019d18ae-b15b-70e9-ac67-64948e87cc5e`
- victim owner page ID remained: `019d18ae-b12f-75ec-8c1c-5aff3ba6be9c`
- server response to overwrite request: `200 OK`
- victim file SHA-256 before overwrite:

```text
686a0a0ede90ece1cbb975bb29304a6c3a90373a9c3ab2496345cf7ca59cc8fa
```

- victim file SHA-256 after overwrite:

```text
e0168298846cdaf75c4d880f4b721d7c0ef0ef310f75617bf2b833af34cdbeba
```

- attacker payload SHA-256:

```text
e0168298846cdaf75c4d880f4b721d7c0ef0ef310f75617bf2b833af34cdbeba
```

- mounted storage confirmed that the victim path now contained:

```text
Attacker replacement from another page
```

That is a complete end-to-end overwrite proof, not just a theoretical source review.

---

## Why the PoC Was Chosen This Way

I used two styles of proof during triage:

- a narrow standalone harness that mirrored the vulnerable overwrite logic, and
- a full live HTTP exploit against a disposable Docmost instance

The standalone harness was useful for isolating the boolean logic failure.

The live HTTP PoC was the stronger artifact because it proved the full security story:

- page authorization succeeded on the attacker page
- the victim `attachmentId` was accepted
- the overwrite request returned success
- the victim page remained the logical owner
- the stored bytes on disk changed to attacker-controlled content

That distinction matters in access-control bugs.

"the condition is wrong" is not enough by itself.

"the condition is wrong, and the application can be driven end to end into a persistent unauthorized overwrite" is the complete case.

---

## Fix Analysis

The fix shipped in **`v0.71.0`** and changed the overwrite guard from `&&` to `||`:

```ts
if (
  existingAttachment.pageId !== pageId ||
  existingAttachment.fileExt !== preparedFile.fileExtension ||
  existingAttachment.workspaceId !== workspaceId
) {
  throw new BadRequestException("File attachment does not match");
}
```

That patch is minimal, direct, and correct for the bug that was reported.

It restores the right rule:

**overwrite is allowed only when the existing attachment exactly matches the authorized page/workspace/type assumptions.**

Once the guard rejects on any mismatch:

- cross-page overwrites fail
- cross-workspace overwrites fail
- type/extension mismatches fail

This was the right kind of fix:

- no redesign
- no vague compatibility logic
- no attempt to "best effort" recover

Just a strict binding between the authorized page and the overwrite target.

There is still a broader engineering lesson here:

generic upload endpoints that also serve in-place update flows should be treated as high-risk API surfaces.

Even when the immediate bug is fixed, stronger long-term designs are:

- dedicated update endpoints for diagram save flows
- immutable binding checks on filename as well as attachment ID
- regression coverage that explicitly models cross-page overwrite attempts

But for the vulnerability itself, the published patch closed the core issue cleanly.

---

## Regression Cases That Matter

Whether or not the project added its own private tests around the fix, these are the cases that matter for long-term coverage:

- overwrite with the same page ID and same attachment ID should succeed
- overwrite with a different page ID and same workspace should fail
- overwrite with a different workspace should fail
- overwrite with mismatched file extension should fail
- overwrite using a known diagram filename but foreign attachment ID should fail
- overwrite should never silently preserve victim ownership after attacker-controlled bytes are written

The point of these tests is not just correctness.

It is to lock the authorization binding down so future "helpful" upload refactors do not reopen the same class of bug.

---

## Severity and Classification

The published advisory classified this issue as:

- **CWE-639**: Authorization Bypass Through User-Controlled Key
- **CVSS v3.1**:

```text
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:L
```

That lands at **7.1 / High**, which is the right conclusion.

The important metric here is Integrity.

This was not a low-grade metadata bug.
The attacker fully controlled the replacement bytes written to another page's attachment path, and the victim page continued to serve the modified object afterward.

That is exactly the kind of stored cross-record tampering that deserves **Integrity High**.

Availability remaining Low also makes sense because corrupting a diagram or attached document can make the victim content unusable, but the primary impact is still unauthorized modification rather than complete service disruption.

---

## Disclosure

I reported the issue privately through GitHub Security Advisories with:

- root-cause analysis
- a live HTTP PoC
- request/response evidence
- before/after file hashes
- a pinned disposable lab setup

The issue was accepted by the maintainer, assigned **CVE-2026-34213**, and published on **April 14, 2026**.

The public advisory lists:

- affected versions: `>= v0.3.0`
- patched version: `v0.71.0`

That history also matched my local source review:
the vulnerable overwrite logic was present in the earliest tagged release I checked in the vulnerable line.

---

## What This Bug Actually Teaches

The interesting lesson here is not merely "use `||` instead of `&&`."

That is the symptom.

The deeper lesson is:

> if one user-controlled field proves authorization and another user-controlled field selects the object being updated, those two fields must be bound together explicitly and exactly.

That rule shows up everywhere:

- document attachments
- profile media
- cloud object references
- issue/comment edits
- background job reprocessing

The moment a system says:

- "you may edit page X"
- "please also tell me which existing record to update"

it has created a security boundary that must be enforced with exact-match invariants.

Anything softer than that turns into a user-controlled-key bug sooner or later.

This issue also reinforces a second point that is easy to underestimate:

**small boolean mistakes in guard code can have first-order security consequences.**

A three-clause condition that "looks reasonable" at a glance was enough to invert the protection model for the overwrite path.

That is why these surfaces deserve deliberate review rather than casual confidence.

---

## Key Points

- Docmost used one endpoint for both new uploads and in-place attachment updates.
- Authorization was checked against the caller-supplied `pageId`, but overwrite target selection used a separate caller-supplied `attachmentId`.
- The overwrite guard rejected only when all mismatch conditions were true at once.
- In the same-workspace attack case, that check failed open.
- The service rebuilt the storage path from the victim attachment ID and wrote attacker-controlled bytes into it.
- The attachment record stayed bound to the victim page after the overwrite.
- Deterministic diagram filenames made exploitation especially practical.
- The fix in `v0.71.0` correctly changed the guard to reject on any mismatch.

---

## Final Words

This vulnerability was not about exotic storage behavior.

It was about an update path trusting an attacker-selected object identifier more than it should have.

Docmost proved edit access on one page, accepted an existing attachment ID from another page, and then let a flawed overwrite check turn that mismatch into a successful cross-page file replacement.

That is why it became **CVE-2026-34213**.

The patch in `v0.71.0` fixed the immediate issue cleanly, but the broader lesson remains valuable:

when authorization and object selection are split across separate user-controlled fields, exact binding is the security property.