jsPDF has HTML Injection in New Window paths
Description
jsPDF is a library to generate PDFs in JavaScript. Prior to version 4.2.1, user control of the options argument of the output function allows attackers to inject arbitrary HTML (such as scripts) into the browser context the created PDF is opened in. The vulnerability can be exploited in the following scenario: the attacker provides values for the output options, for example via a web interface. These values are then passed unsanitized (automatically or semi-automatically) to the attack victim. The victim creates and opens a PDF with the attack vector using one of the vulnerable method overloads inside their browser. The attacker can thus inject scripts that run in the victims browser context and can extract or modify secrets from this context. The vulnerability has been fixed in jspdf@4.2.1. As a workaround, sanitize user input before passing it to the output method.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
jspdfnpm | < 4.2.1 | 4.2.1 |
Affected products
1Patches
12 files changed · +236 −44
src/jspdf.js+93 −44 modified@@ -3007,6 +3007,47 @@ function jsPDF(options) { }); }); + var clearDomNode = function(node) { + while (node.firstChild) { + node.removeChild(node.firstChild); + } + }; + + var initializeNewWindow = function(window) { + var targetDocument = window.document; + var html = targetDocument.documentElement; + var head = targetDocument.head; + var body = targetDocument.body; + var style; + + if (!head) { + head = targetDocument.createElement("head"); + html.appendChild(head); + } + + if (!body) { + body = targetDocument.createElement("body"); + html.appendChild(body); + } + + clearDomNode(head); + clearDomNode(body); + + style = targetDocument.createElement("style"); + style.appendChild( + targetDocument.createTextNode( + "html, body { padding: 0; margin: 0; } iframe { width: 100%; height: 100%; border: 0;}" + ) + ); + + head.appendChild(style); + + return { + document: targetDocument, + body: body + }; + }; + /** * Generates the PDF document. * @@ -3084,7 +3125,7 @@ function jsPDF(options) { } return ( "data:application/pdf;filename=" + - options.filename + + encodeURIComponent(options.filename) + ";base64," + dataURI ); @@ -3094,29 +3135,34 @@ function jsPDF(options) { ) { var pdfObjectUrl = "https://cdnjs.cloudflare.com/ajax/libs/pdfobject/2.1.1/pdfobject.min.js"; - var integrity = - ' integrity="sha512-4ze/a9/4jqu+tX9dfOqJYSvyYd5M6qum/3HpCLr+/Jqf0whc37VUbkpNGHR7/8pSnCFw47T1fmIpwBV7UySh3g==" crossorigin="anonymous"'; + var useDefaultPdfObjectUrl = !options.pdfObjectUrl; - if (options.pdfObjectUrl) { + if (!useDefaultPdfObjectUrl) { pdfObjectUrl = options.pdfObjectUrl; - integrity = ""; } - var htmlForNewWindow = - "<html>" + - '<style>html, body { padding: 0; margin: 0; } iframe { width: 100%; height: 100%; border: 0;} </style><body><script src="' + - pdfObjectUrl + - '"' + - integrity + - '></script><script >PDFObject.embed("' + - this.output("dataurlstring") + - '", ' + - JSON.stringify(options) + - ");</script></body></html>"; var nW = globalObject.open(); if (nW !== null) { - nW.document.write(htmlForNewWindow); + var initializedPdfObjectWindow = initializeNewWindow(nW); + var pdfObjectScript = initializedPdfObjectWindow.document.createElement( + "script" + ); + var scope = this; + + pdfObjectScript.src = pdfObjectUrl; + + if (useDefaultPdfObjectUrl) { + pdfObjectScript.integrity = + "sha512-4ze/a9/4jqu+tX9dfOqJYSvyYd5M6qum/3HpCLr+/Jqf0whc37VUbkpNGHR7/8pSnCFw47T1fmIpwBV7UySh3g=="; + pdfObjectScript.crossOrigin = "anonymous"; + } + + pdfObjectScript.onload = function() { + nW.PDFObject.embed(scope.output("dataurlstring"), options); + }; + + initializedPdfObjectWindow.body.appendChild(pdfObjectScript); } return nW; } else { @@ -3129,30 +3175,33 @@ function jsPDF(options) { Object.prototype.toString.call(globalObject) === "[object Window]" ) { var pdfJsUrl = options.pdfJsUrl || "examples/PDF.js/web/viewer.html"; - var htmlForPDFjsNewWindow = - "<html>" + - "<style>html, body { padding: 0; margin: 0; } iframe { width: 100%; height: 100%; border: 0;} </style>" + - '<body><iframe id="pdfViewer" src="' + - pdfJsUrl + - "?file=&downloadName=" + - options.filename + - '" width="500px" height="400px" />' + - "</body></html>"; var PDFjsNewWindow = globalObject.open(); if (PDFjsNewWindow !== null) { - PDFjsNewWindow.document.write(htmlForPDFjsNewWindow); + var initializedPdfJsWindow = initializeNewWindow(PDFjsNewWindow); + var pdfViewer = initializedPdfJsWindow.document.createElement( + "iframe" + ); + var pdfJsQueryChar = pdfJsUrl.indexOf("?") === -1 ? "?" : "&"; var scope = this; - PDFjsNewWindow.document.documentElement.querySelector( - "#pdfViewer" - ).onload = function() { + + pdfViewer.id = "pdfViewer"; + pdfViewer.width = "500px"; + pdfViewer.height = "400px"; + pdfViewer.src = + pdfJsUrl + + pdfJsQueryChar + + "file=&downloadName=" + + encodeURIComponent(options.filename); + + pdfViewer.onload = function() { PDFjsNewWindow.document.title = options.filename; - PDFjsNewWindow.document.documentElement - .querySelector("#pdfViewer") - .contentWindow.PDFViewerApplication.open( - scope.output("bloburl") - ); + pdfViewer.contentWindow.PDFViewerApplication.open( + scope.output("bloburl") + ); }; + + initializedPdfJsWindow.body.appendChild(pdfViewer); } return PDFjsNewWindow; } else { @@ -3164,17 +3213,17 @@ function jsPDF(options) { if ( Object.prototype.toString.call(globalObject) === "[object Window]" ) { - var htmlForDataURLNewWindow = - "<html>" + - "<style>html, body { padding: 0; margin: 0; } iframe { width: 100%; height: 100%; border: 0;} </style>" + - "<body>" + - '<iframe src="' + - this.output("datauristring", options) + - '"></iframe>' + - "</body></html>"; var dataURLNewWindow = globalObject.open(); if (dataURLNewWindow !== null) { - dataURLNewWindow.document.write(htmlForDataURLNewWindow); + var initializedDataUrlWindow = initializeNewWindow( + dataURLNewWindow + ); + var dataUrlFrame = initializedDataUrlWindow.document.createElement( + "iframe" + ); + + dataUrlFrame.src = this.output("datauristring", options); + initializedDataUrlWindow.body.appendChild(dataUrlFrame); dataURLNewWindow.document.title = options.filename; } if (dataURLNewWindow || typeof safari === "undefined")
test/specs/init.spec.js+143 −0 modified@@ -88,6 +88,14 @@ describe("Core: Initialization Options", () => { }); if (global.isNode !== true) { + const createPopupWindow = () => { + const popupDocument = document.implementation.createHTMLDocument(""); + + return { + document: popupDocument + }; + }; + xit("should open a new window", () => { if (navigator.userAgent.indexOf("Trident") !== -1) { console.warn("Skipping IE for new window test"); @@ -98,6 +106,141 @@ describe("Core: Initialization Options", () => { doc.output("dataurlnewwindow"); // expect(doc.output('dataurlnewwindow').Window).toEqual(jasmine.any(Function)) }); + + it("should safely render dataurlnewwindow content with attacker controlled filenames", () => { + const doc = jsPDF(); + const popupWindow = createPopupWindow(); + const payload = '"></iframe><script>window.__xss = true</script>'; + + spyOn(global, "open").and.returnValue(popupWindow); + + doc.output("dataurlnewwindow", { filename: payload }); + + expect(popupWindow.document.title).toEqual(payload); + expect(popupWindow.document.querySelectorAll("script").length).toEqual(0); + expect(popupWindow.document.querySelectorAll("iframe").length).toEqual(1); + }); + + it("should safely render pdfjsnewwindow content with attacker controlled filenames", () => { + const doc = jsPDF(); + const popupWindow = createPopupWindow(); + const payload = '"></iframe><script>window.__xss = true</script>'; + let viewerFrame; + + spyOn(global, "open").and.returnValue(popupWindow); + + doc.output("pdfjsnewwindow", { + filename: payload, + pdfJsUrl: "viewer.html" + }); + + viewerFrame = popupWindow.document.querySelector("#pdfViewer"); + Object.defineProperty(viewerFrame, "contentWindow", { + value: { + PDFViewerApplication: { + open: jasmine.createSpy("open") + } + } + }); + + viewerFrame.onload(); + + expect(popupWindow.document.title).toEqual(payload); + expect(popupWindow.document.querySelectorAll("script").length).toEqual(0); + expect(viewerFrame.src).toContain(encodeURIComponent(payload)); + expect( + viewerFrame.contentWindow.PDFViewerApplication.open + ).toHaveBeenCalled(); + }); + + it("should safely render pdfobjectnewwindow content with attacker controlled options", () => { + const doc = jsPDF(); + const popupWindow = createPopupWindow(); + const payload = "</script><script>window.__xss = true</script>"; + let loaderScript; + + popupWindow.PDFObject = { + embed: jasmine.createSpy("embed") + }; + + spyOn(global, "open").and.returnValue(popupWindow); + + doc.output("pdfobjectnewwindow", { + filename: payload, + pdfObjectUrl: "https://example.com/pdfobject.js", + customOption: payload + }); + + loaderScript = popupWindow.document.querySelector("script"); + loaderScript.onload(); + + expect(popupWindow.document.querySelectorAll("script").length).toEqual(1); + expect(popupWindow.PDFObject.embed).toHaveBeenCalledWith( + jasmine.any(String), + jasmine.objectContaining({ + filename: payload, + customOption: payload + }) + ); + }); + + it("should set SRI attributes when using default pdfobject URL", () => { + const doc = jsPDF(); + const popupWindow = createPopupWindow(); + + popupWindow.PDFObject = { embed: jasmine.createSpy("embed") }; + spyOn(global, "open").and.returnValue(popupWindow); + + doc.output("pdfobjectnewwindow", { filename: "test.pdf" }); + var loaderScript = popupWindow.document.querySelector("script"); + expect(loaderScript.integrity).toBeTruthy(); + expect(loaderScript.crossOrigin).toEqual("anonymous"); + }); + + it("should omit SRI attributes when using custom pdfobject URL", () => { + const doc = jsPDF(); + const popupWindow = createPopupWindow(); + + popupWindow.PDFObject = { embed: jasmine.createSpy("embed") }; + spyOn(global, "open").and.returnValue(popupWindow); + + doc.output("pdfobjectnewwindow", { + filename: "test.pdf", + pdfObjectUrl: "https://example.com/pdfobject.js" + }); + var loaderScript = popupWindow.document.querySelector("script"); + expect(loaderScript.integrity).toBeFalsy(); + expect(loaderScript.src).toContain("example.com"); + }); + + it("should encode filename in datauristring to prevent data URI corruption", () => { + const doc = jsPDF(); + const payload = "evil;base64,PHNjcmlwdD4=;fakeparam"; + + const result = doc.output("datauristring", { filename: payload }); + expect(result).toContain("filename=" + encodeURIComponent(payload)); + expect(result).not.toContain("filename=" + payload); + }); + + it("should safely handle pdfjsnewwindow with malicious pdfJsUrl", () => { + const doc = jsPDF(); + const popupWindow = createPopupWindow(); + const maliciousUrl = '" onload="alert(1)" data-x="'; + + spyOn(global, "open").and.returnValue(popupWindow); + + doc.output("pdfjsnewwindow", { + filename: "test.pdf", + pdfJsUrl: maliciousUrl + }); + + const iframe = popupWindow.document.querySelector("#pdfViewer"); + // DOM API sets src safely - no attribute injection possible + expect(iframe).not.toBeNull(); + expect(popupWindow.document.querySelectorAll("script").length).toEqual(0); + // The iframe count should be exactly 1 (no injected elements) + expect(popupWindow.document.querySelectorAll("iframe").length).toEqual(1); + }); } const renderBoxes = doc => {
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-wfv2-pwc8-crg5ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-31938ghsaADVISORY
- github.com/parallax/jsPDF/commit/87a40bbd07e6b30575196370670b41f264aa78d7ghsax_refsource_MISCWEB
- github.com/parallax/jsPDF/releases/tag/v4.2.1ghsax_refsource_MISCWEB
- github.com/parallax/jsPDF/security/advisories/GHSA-wfv2-pwc8-crg5ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.