Skip to content
KitploitKITPLOIT
ToolsExploitsBlog
Log in
Submit
ToolsExploitsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

FeedsContactPrivacy© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2026-34212 — Docmost accepted a javascript: URL inside an attachment node, preserved it through storage and rendering, and turned it back into a clickable anchor in the Docmost origin. | Kitploit
Tools/GitHubGitHub/0xmrma/cve-2026-34212
Vulnerability AnalysisExploitationWeb SecurityPenetration TestingPapers & ResearchLearning & Education
GitHub0xmrma/cve-2026-34212

CVE-2026-34212

Docmost accepted a javascript: URL inside an attachment node, preserved it through storage and rendering, and turned it back into a clickable anchor in the Docmost origin.

View Repository
43 months agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

CVE-2026-34212

Docmost accepted a javascript: URL inside an attachment node, preserved it through storage and rendering, and turned it back into a clickable anchor in the Docmost origin.

Intro

I identified, responsibly disclosed, and reproduced a High-severity stored XSS issue 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 sat in a place that is easy to miss in rich-text systems:

not in the ordinary link extension, but in a separate custom node type used for file attachments.

I was reviewing the editor pipeline with a very specific question in mind:

if normal links block javascript: URLs, do attachment nodes enforce the same rule before they reach an anchor sink?

In vulnerable versions, they did not.

Docmost accepted a malicious attachment node in page JSON, stored its url attribute unchanged, and later rendered that value back into a clickable <a href="javascript:..."> element.

That issue became CVE-2026-34212.

Docmost: docmost/docmost
Advisory: GHSA-cf68-cff9-hq4w
CVE: CVE-2026-34212
Patched in: v0.71.0

photo0

Attack Chain

attacker-controlled attachment node URL -> page JSON accepted and stored unchanged -> HTML/React rendering turns that URL into anchor href -> victim clicks attachment action -> attacker-controlled JavaScript executes in the Docmost origin


What This Part of Docmost Does

Docmost stores page content in a ProseMirror/Tiptap-compatible JSON format.

That content model includes custom block nodes for things like:

  • images
  • diagrams
  • embeds
  • attachments

The attachment node stores fields such as:

  • url
  • name
  • mime
  • size
  • attachmentId

The server accepts page content in several formats:

  • json
  • markdown
  • html

and normalizes it into ProseMirror JSON before storing it.

That means any node type that can carry a URL is part of a direct trust boundary.

If one of those node types eventually renders into an <a href>, URL scheme handling is not optional. It is part of the security model.


Why This Surface Was Worth Looking At

Custom editor extensions are a frequent source of security drift.

The base system may already know how to handle dangerous URLs correctly, but each custom node still has to reapply the same rules at its own sinks.

That creates a predictable review strategy:

  • find every node type that stores a URL-like field
  • trace where that field is accepted
  • trace where that field is rendered
  • compare its sanitization behavior with the platform's normal link handling

That is exactly what exposed this bug.

Docmost's normal link extension already treated javascript: as dangerous.

Its attachment node did not.

Once you see that asymmetry, the security question becomes obvious:

can I persist an attachment node whose url is javascript: and get it rendered back into a live anchor?

The answer was yes.


Root Cause

The root cause was inconsistent URL sanitization across content node types.

The server-side content path accepted arbitrary attachment URLs as long as the overall content matched the ProseMirror schema.

In the vulnerable version:

  • CreatePageDto accepted content?: string | object
  • PageService.parseProsemirrorContent() normalized markdown, html, or json
  • the server then called jsonToNode(prosemirrorJson)
  • if schema validation passed, the content was stored

That validation step checked structural validity, not URL safety.

The critical part of the vulnerable server logic was effectively:

prosemirrorJson = content;
jsonToNode(prosemirrorJson);
return prosemirrorJson;

No attachment URL scheme normalization happened there.

Later, the attachment extension rendered the attacker-controlled value directly.

The vulnerable attachment node did this:

url: {
  default: "",
  parseHTML: (element) => element.getAttribute("data-attachment-url"),
  renderHTML: (attributes) => ({
    "data-attachment-url": attributes.url,
  }),
},

and then:

[
  "a",
  {
    href: HTMLAttributes["data-attachment-url"],
    class: "attachment",
    target: "blank",
  },
  `${HTMLAttributes["data-attachment-name"]}`,
]

On the client side, the React node view wrapped that again in:

<a href={getFileUrl(url)} target="_blank">

But getFileUrl() only special-cased:

  • absolute http URLs
  • /api/...
  • /files/...

Anything else was returned unchanged.

So a payload like:

javascript:alert(document.domain)

survived:

  • JSON storage
  • server-side schema validation
  • HTML rendering
  • client-side URL handling

That alone would already be enough for stored XSS.

What makes the root cause especially clear is the comparison point.

Docmost's normal link extension explicitly blocked javascript::

  • it rejected javascript: in parseHTML()
  • it blanked out a javascript: href in renderHTML()

So the product already knew this scheme was dangerous.

The attachment node simply failed to apply the same policy.

That is why this was not "generic XSS in the editor."

It was a node-specific trust-boundary gap.


Why This Is a Security Issue, Not Just Missing Sanitization

This bug was not merely about unsafe HTML aesthetics.

It allowed an attacker who could edit a page to persist a malicious payload that would later execute in the Docmost origin when another user interacted with the rendered attachment.

That matters because in-origin script can:

  • read data the victim can access
  • issue authenticated requests as the victim
  • modify content the victim is allowed to modify
  • abuse any DOM or API surface exposed to the session

The requirement for a click does not reduce this to a trivial issue.

The click is part of the normal product behavior: the UI intentionally presents the attachment as an actionable link/icon.

So the security question is not "can the attacker force arbitrary JS without any interaction?"

The real question is:

does the application store attacker-controlled script-bearing content and later present it back to other users as a trusted interaction path?

In vulnerable versions, it did.

That is stored XSS.


Why Exploitation Was Practical

The exploit path was straightforward:

  • any user with page edit rights could plant the payload
  • the malicious URL survived storage unchanged
  • the page rendered normally
  • viewers only needed standard access to the page
  • one click on the attachment action was enough to trigger execution

This also made higher-privileged users realistic targets.

If a workspace owner, admin, or broadly trusted editor viewed attacker-controlled content and clicked the attachment action, the attacker's script would run in that more privileged session context.

That is the important practical point:

the attacker's privilege requirement was only low. The victim's privilege level determined how much value the XSS session carried.


Proof of Concept

I validated the issue live against Docmost v0.70.3.

The PoC used only normal HTTP requests and the application's own page APIs.

The flow was:

Download Tool