Skip to content

Commit 87a40bb

Browse files
Merge commit from fork
* Fix popup rendering for new window outputs * Encode filename in data URI, add edge case tests - Encode options.filename in datauristring to prevent data URI structure corruption via semicolons/commas - Add tests: SRI on default pdfobject URL, data URI filename encoding, malicious pdfJsUrl attribute injection attempt * Fix SRI test: split into default and custom URL cases The previous test claimed to cover both default and custom URL paths but only checked the default. Now split into two separate tests that each verify what they claim. --------- Co-authored-by: Doruk <peak@peaktwilight.com>
1 parent b1607a9 commit 87a40bb

File tree

2 files changed

+236
-44
lines changed

2 files changed

+236
-44
lines changed

src/jspdf.js

Lines changed: 93 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -3007,6 +3007,47 @@ function jsPDF(options) {
30073007
});
30083008
});
30093009

3010+
var clearDomNode = function(node) {
3011+
while (node.firstChild) {
3012+
node.removeChild(node.firstChild);
3013+
}
3014+
};
3015+
3016+
var initializeNewWindow = function(window) {
3017+
var targetDocument = window.document;
3018+
var html = targetDocument.documentElement;
3019+
var head = targetDocument.head;
3020+
var body = targetDocument.body;
3021+
var style;
3022+
3023+
if (!head) {
3024+
head = targetDocument.createElement("head");
3025+
html.appendChild(head);
3026+
}
3027+
3028+
if (!body) {
3029+
body = targetDocument.createElement("body");
3030+
html.appendChild(body);
3031+
}
3032+
3033+
clearDomNode(head);
3034+
clearDomNode(body);
3035+
3036+
style = targetDocument.createElement("style");
3037+
style.appendChild(
3038+
targetDocument.createTextNode(
3039+
"html, body { padding: 0; margin: 0; } iframe { width: 100%; height: 100%; border: 0;}"
3040+
)
3041+
);
3042+
3043+
head.appendChild(style);
3044+
3045+
return {
3046+
document: targetDocument,
3047+
body: body
3048+
};
3049+
};
3050+
30103051
/**
30113052
* Generates the PDF document.
30123053
*
@@ -3084,7 +3125,7 @@ function jsPDF(options) {
30843125
}
30853126
return (
30863127
"data:application/pdf;filename=" +
3087-
options.filename +
3128+
encodeURIComponent(options.filename) +
30883129
";base64," +
30893130
dataURI
30903131
);
@@ -3094,29 +3135,34 @@ function jsPDF(options) {
30943135
) {
30953136
var pdfObjectUrl =
30963137
"https://cdnjs.cloudflare.com/ajax/libs/pdfobject/2.1.1/pdfobject.min.js";
3097-
var integrity =
3098-
' integrity="sha512-4ze/a9/4jqu+tX9dfOqJYSvyYd5M6qum/3HpCLr+/Jqf0whc37VUbkpNGHR7/8pSnCFw47T1fmIpwBV7UySh3g==" crossorigin="anonymous"';
3138+
var useDefaultPdfObjectUrl = !options.pdfObjectUrl;
30993139

3100-
if (options.pdfObjectUrl) {
3140+
if (!useDefaultPdfObjectUrl) {
31013141
pdfObjectUrl = options.pdfObjectUrl;
3102-
integrity = "";
31033142
}
31043143

3105-
var htmlForNewWindow =
3106-
"<html>" +
3107-
'<style>html, body { padding: 0; margin: 0; } iframe { width: 100%; height: 100%; border: 0;} </style><body><script src="' +
3108-
pdfObjectUrl +
3109-
'"' +
3110-
integrity +
3111-
'></script><script >PDFObject.embed("' +
3112-
this.output("dataurlstring") +
3113-
'", ' +
3114-
JSON.stringify(options) +
3115-
");</script></body></html>";
31163144
var nW = globalObject.open();
31173145

31183146
if (nW !== null) {
3119-
nW.document.write(htmlForNewWindow);
3147+
var initializedPdfObjectWindow = initializeNewWindow(nW);
3148+
var pdfObjectScript = initializedPdfObjectWindow.document.createElement(
3149+
"script"
3150+
);
3151+
var scope = this;
3152+
3153+
pdfObjectScript.src = pdfObjectUrl;
3154+
3155+
if (useDefaultPdfObjectUrl) {
3156+
pdfObjectScript.integrity =
3157+
"sha512-4ze/a9/4jqu+tX9dfOqJYSvyYd5M6qum/3HpCLr+/Jqf0whc37VUbkpNGHR7/8pSnCFw47T1fmIpwBV7UySh3g==";
3158+
pdfObjectScript.crossOrigin = "anonymous";
3159+
}
3160+
3161+
pdfObjectScript.onload = function() {
3162+
nW.PDFObject.embed(scope.output("dataurlstring"), options);
3163+
};
3164+
3165+
initializedPdfObjectWindow.body.appendChild(pdfObjectScript);
31203166
}
31213167
return nW;
31223168
} else {
@@ -3129,30 +3175,33 @@ function jsPDF(options) {
31293175
Object.prototype.toString.call(globalObject) === "[object Window]"
31303176
) {
31313177
var pdfJsUrl = options.pdfJsUrl || "examples/PDF.js/web/viewer.html";
3132-
var htmlForPDFjsNewWindow =
3133-
"<html>" +
3134-
"<style>html, body { padding: 0; margin: 0; } iframe { width: 100%; height: 100%; border: 0;} </style>" +
3135-
'<body><iframe id="pdfViewer" src="' +
3136-
pdfJsUrl +
3137-
"?file=&downloadName=" +
3138-
options.filename +
3139-
'" width="500px" height="400px" />' +
3140-
"</body></html>";
31413178
var PDFjsNewWindow = globalObject.open();
31423179

31433180
if (PDFjsNewWindow !== null) {
3144-
PDFjsNewWindow.document.write(htmlForPDFjsNewWindow);
3181+
var initializedPdfJsWindow = initializeNewWindow(PDFjsNewWindow);
3182+
var pdfViewer = initializedPdfJsWindow.document.createElement(
3183+
"iframe"
3184+
);
3185+
var pdfJsQueryChar = pdfJsUrl.indexOf("?") === -1 ? "?" : "&";
31453186
var scope = this;
3146-
PDFjsNewWindow.document.documentElement.querySelector(
3147-
"#pdfViewer"
3148-
).onload = function() {
3187+
3188+
pdfViewer.id = "pdfViewer";
3189+
pdfViewer.width = "500px";
3190+
pdfViewer.height = "400px";
3191+
pdfViewer.src =
3192+
pdfJsUrl +
3193+
pdfJsQueryChar +
3194+
"file=&downloadName=" +
3195+
encodeURIComponent(options.filename);
3196+
3197+
pdfViewer.onload = function() {
31493198
PDFjsNewWindow.document.title = options.filename;
3150-
PDFjsNewWindow.document.documentElement
3151-
.querySelector("#pdfViewer")
3152-
.contentWindow.PDFViewerApplication.open(
3153-
scope.output("bloburl")
3154-
);
3199+
pdfViewer.contentWindow.PDFViewerApplication.open(
3200+
scope.output("bloburl")
3201+
);
31553202
};
3203+
3204+
initializedPdfJsWindow.body.appendChild(pdfViewer);
31563205
}
31573206
return PDFjsNewWindow;
31583207
} else {
@@ -3164,17 +3213,17 @@ function jsPDF(options) {
31643213
if (
31653214
Object.prototype.toString.call(globalObject) === "[object Window]"
31663215
) {
3167-
var htmlForDataURLNewWindow =
3168-
"<html>" +
3169-
"<style>html, body { padding: 0; margin: 0; } iframe { width: 100%; height: 100%; border: 0;} </style>" +
3170-
"<body>" +
3171-
'<iframe src="' +
3172-
this.output("datauristring", options) +
3173-
'"></iframe>' +
3174-
"</body></html>";
31753216
var dataURLNewWindow = globalObject.open();
31763217
if (dataURLNewWindow !== null) {
3177-
dataURLNewWindow.document.write(htmlForDataURLNewWindow);
3218+
var initializedDataUrlWindow = initializeNewWindow(
3219+
dataURLNewWindow
3220+
);
3221+
var dataUrlFrame = initializedDataUrlWindow.document.createElement(
3222+
"iframe"
3223+
);
3224+
3225+
dataUrlFrame.src = this.output("datauristring", options);
3226+
initializedDataUrlWindow.body.appendChild(dataUrlFrame);
31783227
dataURLNewWindow.document.title = options.filename;
31793228
}
31803229
if (dataURLNewWindow || typeof safari === "undefined")

test/specs/init.spec.js

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,14 @@ describe("Core: Initialization Options", () => {
8888
});
8989

9090
if (global.isNode !== true) {
91+
const createPopupWindow = () => {
92+
const popupDocument = document.implementation.createHTMLDocument("");
93+
94+
return {
95+
document: popupDocument
96+
};
97+
};
98+
9199
xit("should open a new window", () => {
92100
if (navigator.userAgent.indexOf("Trident") !== -1) {
93101
console.warn("Skipping IE for new window test");
@@ -98,6 +106,141 @@ describe("Core: Initialization Options", () => {
98106
doc.output("dataurlnewwindow");
99107
// expect(doc.output('dataurlnewwindow').Window).toEqual(jasmine.any(Function))
100108
});
109+
110+
it("should safely render dataurlnewwindow content with attacker controlled filenames", () => {
111+
const doc = jsPDF();
112+
const popupWindow = createPopupWindow();
113+
const payload = '"></iframe><script>window.__xss = true</script>';
114+
115+
spyOn(global, "open").and.returnValue(popupWindow);
116+
117+
doc.output("dataurlnewwindow", { filename: payload });
118+
119+
expect(popupWindow.document.title).toEqual(payload);
120+
expect(popupWindow.document.querySelectorAll("script").length).toEqual(0);
121+
expect(popupWindow.document.querySelectorAll("iframe").length).toEqual(1);
122+
});
123+
124+
it("should safely render pdfjsnewwindow content with attacker controlled filenames", () => {
125+
const doc = jsPDF();
126+
const popupWindow = createPopupWindow();
127+
const payload = '"></iframe><script>window.__xss = true</script>';
128+
let viewerFrame;
129+
130+
spyOn(global, "open").and.returnValue(popupWindow);
131+
132+
doc.output("pdfjsnewwindow", {
133+
filename: payload,
134+
pdfJsUrl: "viewer.html"
135+
});
136+
137+
viewerFrame = popupWindow.document.querySelector("#pdfViewer");
138+
Object.defineProperty(viewerFrame, "contentWindow", {
139+
value: {
140+
PDFViewerApplication: {
141+
open: jasmine.createSpy("open")
142+
}
143+
}
144+
});
145+
146+
viewerFrame.onload();
147+
148+
expect(popupWindow.document.title).toEqual(payload);
149+
expect(popupWindow.document.querySelectorAll("script").length).toEqual(0);
150+
expect(viewerFrame.src).toContain(encodeURIComponent(payload));
151+
expect(
152+
viewerFrame.contentWindow.PDFViewerApplication.open
153+
).toHaveBeenCalled();
154+
});
155+
156+
it("should safely render pdfobjectnewwindow content with attacker controlled options", () => {
157+
const doc = jsPDF();
158+
const popupWindow = createPopupWindow();
159+
const payload = "</script><script>window.__xss = true</script>";
160+
let loaderScript;
161+
162+
popupWindow.PDFObject = {
163+
embed: jasmine.createSpy("embed")
164+
};
165+
166+
spyOn(global, "open").and.returnValue(popupWindow);
167+
168+
doc.output("pdfobjectnewwindow", {
169+
filename: payload,
170+
pdfObjectUrl: "https://example.com/pdfobject.js",
171+
customOption: payload
172+
});
173+
174+
loaderScript = popupWindow.document.querySelector("script");
175+
loaderScript.onload();
176+
177+
expect(popupWindow.document.querySelectorAll("script").length).toEqual(1);
178+
expect(popupWindow.PDFObject.embed).toHaveBeenCalledWith(
179+
jasmine.any(String),
180+
jasmine.objectContaining({
181+
filename: payload,
182+
customOption: payload
183+
})
184+
);
185+
});
186+
187+
it("should set SRI attributes when using default pdfobject URL", () => {
188+
const doc = jsPDF();
189+
const popupWindow = createPopupWindow();
190+
191+
popupWindow.PDFObject = { embed: jasmine.createSpy("embed") };
192+
spyOn(global, "open").and.returnValue(popupWindow);
193+
194+
doc.output("pdfobjectnewwindow", { filename: "test.pdf" });
195+
var loaderScript = popupWindow.document.querySelector("script");
196+
expect(loaderScript.integrity).toBeTruthy();
197+
expect(loaderScript.crossOrigin).toEqual("anonymous");
198+
});
199+
200+
it("should omit SRI attributes when using custom pdfobject URL", () => {
201+
const doc = jsPDF();
202+
const popupWindow = createPopupWindow();
203+
204+
popupWindow.PDFObject = { embed: jasmine.createSpy("embed") };
205+
spyOn(global, "open").and.returnValue(popupWindow);
206+
207+
doc.output("pdfobjectnewwindow", {
208+
filename: "test.pdf",
209+
pdfObjectUrl: "https://example.com/pdfobject.js"
210+
});
211+
var loaderScript = popupWindow.document.querySelector("script");
212+
expect(loaderScript.integrity).toBeFalsy();
213+
expect(loaderScript.src).toContain("example.com");
214+
});
215+
216+
it("should encode filename in datauristring to prevent data URI corruption", () => {
217+
const doc = jsPDF();
218+
const payload = "evil;base64,PHNjcmlwdD4=;fakeparam";
219+
220+
const result = doc.output("datauristring", { filename: payload });
221+
expect(result).toContain("filename=" + encodeURIComponent(payload));
222+
expect(result).not.toContain("filename=" + payload);
223+
});
224+
225+
it("should safely handle pdfjsnewwindow with malicious pdfJsUrl", () => {
226+
const doc = jsPDF();
227+
const popupWindow = createPopupWindow();
228+
const maliciousUrl = '" onload="alert(1)" data-x="';
229+
230+
spyOn(global, "open").and.returnValue(popupWindow);
231+
232+
doc.output("pdfjsnewwindow", {
233+
filename: "test.pdf",
234+
pdfJsUrl: maliciousUrl
235+
});
236+
237+
const iframe = popupWindow.document.querySelector("#pdfViewer");
238+
// DOM API sets src safely - no attribute injection possible
239+
expect(iframe).not.toBeNull();
240+
expect(popupWindow.document.querySelectorAll("script").length).toEqual(0);
241+
// The iframe count should be exactly 1 (no injected elements)
242+
expect(popupWindow.document.querySelectorAll("iframe").length).toEqual(1);
243+
});
101244
}
102245

103246
const renderBoxes = doc => {

0 commit comments

Comments
 (0)