
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.
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.
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:
pageId for a page they were allowed to edit, andattachmentId belonging to a different page in the same workspaceThe 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
Advisory: GHSA-89fp-2hch-j9gp
CVE: CVE-2026-34213
Patched in: v0.71.0
---
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
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:
Whenever an endpoint mixes those two responsibilities, the implementation must bind them together exactly.
Docmost did not.
Mixed create/update endpoints are common places for authorization bugs.
The reason is simple:
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.
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:
AttachmentController.uploadFile() read pageId from multipart form data.validateCanEdit(page, user).attachmentId from the same request.AttachmentService.uploadFile() loaded the existing attachment by that attacker-supplied ID.&& instead of rejecting on any mismatch.The vulnerable guard was:
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:
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 falseOnce 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:
const filePath =
`${getAttachmentFolderPath(AttachmentType.File, workspaceId)}/` +
`${attachmentId}/${preparedFile.fileName}`;
Then, on the update path, Docmost only updated mutable metadata such as:
fileSizeupdatedAtIt 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.
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:
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:
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.
The exploit was especially practical for diagram attachments.
Docmost's client intentionally reuses attachmentId for diagram saves and uses deterministic filenames:
diagram.excalidraw.svgdiagram.drawio.svgThat matters because it lowers the attacker's requirements.
For generic attachments, the attacker needs both:
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:
attachmentIdIn my validation setup, I used exactly that path: