Hall of Fame image from https://openclipart.org/detail/120343/trophy
Back to Hall of Fame Contents Back to Wekan Website

Contents / ExportBleed

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.
  • 1. ExportBleed — stored XSS in HTML board exports through a card-title second parse (CWE-79 Cross-site Scripting)
  • GitHub Security Advisory GHSA-8r5p-4q9j-f5jx, severity High
  • Affected the WeKan HTML board exporter (client/lib/exportHTML.js)
  • Fixed at upcoming WeKan release


Details

1. ExportBleed — stored XSS in HTML board exports through a card-title second parse (CWE-79)

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 &lt;img…&gt; 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.