| CVE | Vulnerability name | Date | Responsible Security Disclosure by | Vulnerabilities |
|---|---|---|---|---|
|
-
|
ExportBleed |
2026-07-22 |
koyokr (GitHub) and xet7 (fix)![]() Privately reported via GitHub Security Advisory GHSA-8r5p-4q9j-f5jx. |
|
WeKan can export a board to a self-contained index.html inside a ZIP. To make
the exported cards interactive, the exporter injects a small click handler
(client/lib/exportHTML.js, addBoardHTMLToZip()) that opens a modal
with the clicked card's title and text.
A board member could store entity-encoded markup in a card title, for example:
EXPORT_XSS_MARKER <img src=x onerror="…">
On the live board this stays completely inert: Blaze {{ }} HTML-escapes the
title, and the +viewer Markdown/sanitizer pipeline neutralizes a literal
handler while preserving the entity payload as text. The problem was the exported
click handler, which read the card title and body through .textContent — which
DECODES HTML entities — and then concatenated those decoded values into
innerHTML:
// vulnerable — a SECOND parse of decoded card text
const titleText = title ? title.textContent.trim() : '…'; // decodes <img…> to <img…>
const allText = this.textContent.trim();
const content = document.createElement('div');
content.innerHTML =
'<h2>' + titleText + '</h2><hr><div>' + allText + '</div>…'; // revives the tag
Assigning the decoded string to innerHTML re-parses it, reviving the
<img … onerror=…> and running it the moment a recipient clicks the
card in the downloaded export. The script runs in the file/export origin, not the WeKan
HTTP origin, but it can read and exfiltrate everything in that document — including cards
that were added after the attacker's board membership was revoked, since the export is
generated by a still-privileged user.
Fix: never place the card title/body back into innerHTML. The modal is
now built from DOM nodes and those two values are assigned through textContent,
so they are inserted as inert text and never re-parsed as markup:
// fixed — build DOM nodes, assign untrusted text via textContent
const titleEl = document.createElement('h2');
titleEl.textContent = titleText; // inert text, never parsed
content.appendChild(titleEl);
const bodyEl = document.createElement('div');
bodyEl.textContent = allText; // inert text, never parsed
content.appendChild(bodyEl);
A regression suite (tests/exportHTMLXss.test.cjs) pins that the card
title/body reach only textContent, that neither value flows into any
innerHTML assignment, and that a textContent assignment does not
revive an entity-decoded <img>. This is the same class of "second parse of
decoded text" defect as prior incomplete-sanitization issues, but here it was a live stored
XSS in the export rather than defence-in-depth.