VYPR
High severityOSV Advisory· Published Jan 14, 2026· Updated Jan 20, 2026

html2pdf.js has a cross-site scripting vulnerability

CVE-2026-22787

Description

html2pdf.js converts any webpage or element into a printable PDF entirely client-side. Prior to 0.14.0, html2pdf.js contains a cross-site scripting (XSS) vulnerability when given a text source rather than an element. This text is not sufficiently sanitized before being attached to the DOM, allowing malicious scripts to be run on the client browser and risking the confidentiality, integrity, and availability of the page's data. This vulnerability has been fixed in html2pdf.js@0.14.0.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

html2pdf.js before 0.14.0 is vulnerable to stored XSS via unsanitized string inputs assigned to innerHTML, allowing arbitrary script execution.

Vulnerability

Overview html2pdf.js is a client-side library that converts HTML to PDF. Versions prior to 0.14.0 contain a cross-site scripting (XSS) vulnerability when the library receives a text string (rather than a DOM element) as input. The string is directly assigned to the innerHTML property of a newly created ` element without sanitization, as shown in the vulnerable code at src/worker.js line 71: return this.set({ src: createElement('div', {innerHTML: src}) }) [3]. This unsanitized assignment occurs in the Worker.prototype.from()` method [3].

Exploitation

An attacker can supply a malicious HTML string containing JavaScript, e.g., via event handlers or other injection vectors. The library then sets this string as innerHTML, and when the element is appended to the DOM, the browser executes the embedded script [3]. While a partial mitigation removes ` tags from the input, it does not block other XSS payloads such as or [3]. No authentication is required; the attacker merely needs to control the string passed to html2pdf()` [1][2].

Impact

Successful exploitation allows arbitrary JavaScript execution in the context of the user's browser. This can lead to session hijacking, theft of sensitive data, defacement, or other unauthorized actions, compromising the confidentiality, integrity, and availability of the page's data [3][4].

Mitigation

The vulnerability has been fixed in html2pdf.js@0.14.0 by properly sanitizing text sources [2]. Users should upgrade to version 0.14.0 or later [1][4]. There is no known workaround for older versions.

AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
html2pdf.jsnpm
< 0.14.00.14.0

Affected products

2

Patches

1
988826e33603

feat: Sanitize text sources (#877)

https://github.com/eKoopmans/html2pdf.jsErik KoopmansJan 12, 2026via ghsa
7 files changed · +33 16
  • demo/blank.html+12 1 modified
    @@ -21,6 +21,17 @@ <h2>Blank page: default</h2>
                         ></test-harness>
     				</template>
     			</d2l-demo-snippet>
    -        </d2l-demo-page>
    +
    +			<h2>Blank page: text source</h2>
    +			<d2l-demo-snippet>
    +				<template>
    +					<test-harness
    +						controls
    +						file="blank"
    +						text-source
    +					></test-harness>
    +				</template>
    +			</d2l-demo-snippet>
    +		</d2l-demo-page>
     	</body>
     </html>
    
  • package.json+1 0 modified
    @@ -28,6 +28,7 @@
       },
       "homepage": "https://ekoopmans.github.io/html2pdf.js/",
       "dependencies": {
    +    "dompurify": "^3.3.1",
         "html2canvas": "^1.0.0",
         "jspdf": "^4.0.0"
       },
    
  • package-lock.json+1 1 modified
    @@ -9,6 +9,7 @@
           "version": "0.13.0",
           "license": "MIT",
           "dependencies": {
    +        "dompurify": "^3.3.1",
             "html2canvas": "^1.0.0",
             "jspdf": "^4.0.0"
           },
    @@ -5211,7 +5212,6 @@
           "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz",
           "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==",
           "license": "(MPL-2.0 OR Apache-2.0)",
    -      "optional": true,
           "optionalDependencies": {
             "@types/trusted-types": "^2.0.7"
           }
    
  • src/utils.js+5 8 modified
    @@ -1,3 +1,5 @@
    +import DOMPurify from 'dompurify';
    +
     // Determine the type of a variable/object.
     export const objType = function objType(obj) {
       var type = typeof obj;
    @@ -14,14 +16,9 @@ export const objType = function objType(obj) {
     // Create an HTML element with optional className, innerHTML, and style.
     export const createElement = function createElement(tagName, opt) {
       var el = document.createElement(tagName);
    -  if (opt.className)  el.className = opt.className;
    -  if (opt.innerHTML) {
    -    el.innerHTML = opt.innerHTML;
    -    var scripts = el.getElementsByTagName('script');
    -    for (var i = scripts.length; i-- > 0; null) {
    -      scripts[i].parentNode.removeChild(scripts[i]);
    -    }
    -  }
    +  if (opt.className) el.className = opt.className;
    +  if (opt.innerHTML) el.innerHTML = DOMPurify.sanitize(opt.innerHTML);
    +
       for (var key in opt.style) {
         el.style[key] = opt.style[key];
       }
    
  • test/util/test-harness.js+11 5 modified
    @@ -2,17 +2,22 @@ import { css, html, LitElement, nothing } from 'lit';
     import { LoadingCompleteMixin } from '@brightspace-ui/core/mixins/loading-complete/loading-complete-mixin.js';
     
     const IFRAME_SCRIPTS_URL = new URL('./iframe-scripts.js', import.meta.url).href;
    +const TEXT_SOURCE = `
    +    <span id="target">Safe</span> text
    +    <img src=x onerror="document.querySelector('#target').innerHTML = 'Onerror'">
    +    <script>document.querySelector('#target').innerHTML = 'Script'</script>
    +`;
     const stub = (object, method, fake) => {
         const original = object[method];
         object[method] = fake;
         return () => object[method] = original;
     };
     
     const commands = {
    -    default: (window, element, settings) => window.html2pdf().set(settings).from(element).outputPdf('arraybuffer'),
    -    legacy: async (window, element, settings) => {
    +    default: (window, src, settings) => window.html2pdf().set(settings).from(src).outputPdf('arraybuffer'),
    +    legacy: async (window, src, settings) => {
             const restore = stub(window.html2pdf.Worker.prototype, 'save', function () { return this.then(function save() { }); });
    -        const arrayBuffer = await window.html2pdf(element, settings).outputPdf('arraybuffer');
    +        const arrayBuffer = await window.html2pdf(src, settings).outputPdf('arraybuffer');
             restore();
             return arrayBuffer;
         },
    @@ -41,6 +46,7 @@ class TestHarness extends LoadingCompleteMixin(LitElement) {
             selector: { type: String },
             settings: { type: String },
             show: { type: String, reflect: true },
    +        textSource: { type: Boolean, attribute: 'text-source' },
             _arrayBuffer: { state: true },
         };
     
    @@ -140,10 +146,10 @@ class TestHarness extends LoadingCompleteMixin(LitElement) {
         }
     
         async _handleScriptLoad() {
    -        const element = this._pdfIframeWindow.document.querySelector(this.selector);
    +        const src = this.textSource ? TEXT_SOURCE : this._pdfIframeWindow.document.querySelector(this.selector);
     
             const command = commands[this.command || 'default'];
    -        const arrayBuffer = await command(this._pdfIframeWindow, element, settings[this.settings || 'default']);
    +        const arrayBuffer = await command(this._pdfIframeWindow, src, settings[this.settings || 'default']);
             await this._pdfIframeWindow.renderPdf(arrayBuffer);
     
             this._resizeIframe(this._pdfIframe, true);
    
  • test/vdiff/golden/html2pdf/chromium/blank-textsource.png+0 0 added
  • test/vdiff/html2pdf.vdiff.js+3 1 modified
    @@ -15,10 +15,11 @@ const conditions = {
       pagebreakCss: { settings: 'pagebreakCss' },
       pagebreakAvoidAll: { settings: 'pagebreakAvoidAll' },
       pagebreakSpecify: { settings: 'pagebreakSpecify' },
    +  textSource: { textSource: true },
     };
     
     const fileConditions = {
    -  'blank': ['default'],
    +  'blank': ['default', 'textSource'],
       'lorem-ipsum': ['default', 'legacy', 'margin'],
       'all-tags': ['default', 'selectCanvas'],
       'css-selectors': ['default', 'selectMainId'],
    @@ -38,6 +39,7 @@ describe('html2pdf', () => {
               selector=${ifDefined(condition.selector)}
               settings=${ifDefined(condition.settings)}
               show="pdf"
    +          text-source=${ifDefined(condition.textSource)}
             ></test-harness>
           `, { viewport });
     
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

8

News mentions

0

No linked articles in our index yet.