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
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:
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:
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:
That was enough.
The exploit crossed page boundaries and crossed space boundaries inside the same workspace, while still satisfying the flawed workspace check.
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:
attachmentId.POST /api/files/upload with:
pageId = attacker page IDattachmentId = victim attachment IDfile = attacker-controlled replacement file using the victim filenameThe minimal request shape was:
The observed live result was:
019d18ae-b176-751c-8525-b5f3cede131d019d18ae-b15b-70e9-ac67-64948e87cc5e019d18ae-b12f-75ec-8c1c-5aff3ba6be9c200 OKThat is a complete end-to-end overwrite proof, not just a theoretical source review.
I used two styles of proof during triage:
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:
attachmentId was acceptedThat 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.
The fix shipped in v0.71.0 and changed the overwrite guard from && to ||:
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:
This was the right kind of fix:
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:
But for the vulnerability itself, the published patch closed the core issue cleanly.
Whether or not the project added its own private tests around the fix, these are the cases that matter for long-term coverage:
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.
The published advisory classified this issue as:
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.
I reported the issue privately through GitHub Security Advisories with:
The issue was accepted by the maintainer, assigned CVE-2026-34213, and published on April 14, 2026.
The public advisory lists:
>= v0.3.0v0.71.0That history also matched my local source review: the vulnerable overwrite logic was present in the earliest tagged release I checked in the vulnerable line.
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:
The moment a system says:
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.
pageId, but overwrite target selection used a separate caller-supplied attachmentId.v0.71.0 correctly changed the guard to reject on any mismatch.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.