Validate explicit destinations on the worker-thread to prevent DataCloneError (issue 17981)

*Note:* This borrows a helper function from the viewer, however the code cannot be directly shared since the worker-thread has access to various primitives.
This commit is contained in:
Jonas Jenwald 2024-04-22 13:47:54 +02:00
parent 522af265a7
commit 7206d0a237
5 changed files with 81 additions and 34 deletions

View file

@ -52,11 +52,57 @@ import { GlobalImageCache } from "./image_utils.js";
import { MetadataParser } from "./metadata_parser.js"; import { MetadataParser } from "./metadata_parser.js";
import { StructTreeRoot } from "./struct_tree.js"; import { StructTreeRoot } from "./struct_tree.js";
function fetchDestination(dest) { function isValidExplicitDest(dest) {
if (!Array.isArray(dest) || dest.length < 2) {
return false;
}
const [page, zoom, ...args] = dest;
if (!(page instanceof Ref) && !Number.isInteger(page)) {
return false;
}
if (!(zoom instanceof Name)) {
return false;
}
let allowNull = true;
switch (zoom.name) {
case "XYZ":
if (args.length !== 3) {
return false;
}
break;
case "Fit":
case "FitB":
return args.length === 0;
case "FitH":
case "FitBH":
case "FitV":
case "FitBV":
if (args.length !== 1) {
return false;
}
break;
case "FitR":
if (args.length !== 4) {
return false;
}
allowNull = false;
break;
default:
return false;
}
for (const arg of args) {
if (!(typeof arg === "number" || (allowNull && arg === null))) {
return false;
}
}
return true;
}
function fetchDest(dest) {
if (dest instanceof Dict) { if (dest instanceof Dict) {
dest = dest.get("D"); dest = dest.get("D");
} }
return Array.isArray(dest) ? dest : null; return isValidExplicitDest(dest) ? dest : null;
} }
function fetchRemoteDest(action) { function fetchRemoteDest(action) {
@ -67,7 +113,7 @@ function fetchRemoteDest(action) {
} }
if (typeof dest === "string") { if (typeof dest === "string") {
return stringToPDFString(dest); return stringToPDFString(dest);
} else if (Array.isArray(dest)) { } else if (isValidExplicitDest(dest)) {
return JSON.stringify(dest); return JSON.stringify(dest);
} }
} }
@ -641,14 +687,14 @@ class Catalog {
dests = Object.create(null); dests = Object.create(null);
if (obj instanceof NameTree) { if (obj instanceof NameTree) {
for (const [key, value] of obj.getAll()) { for (const [key, value] of obj.getAll()) {
const dest = fetchDestination(value); const dest = fetchDest(value);
if (dest) { if (dest) {
dests[stringToPDFString(key)] = dest; dests[stringToPDFString(key)] = dest;
} }
} }
} else if (obj instanceof Dict) { } else if (obj instanceof Dict) {
obj.forEach(function (key, value) { obj.forEach(function (key, value) {
const dest = fetchDestination(value); const dest = fetchDest(value);
if (dest) { if (dest) {
dests[key] = dest; dests[key] = dest;
} }
@ -660,7 +706,7 @@ class Catalog {
getDestination(id) { getDestination(id) {
const obj = this._readDests(); const obj = this._readDests();
if (obj instanceof NameTree) { if (obj instanceof NameTree) {
const dest = fetchDestination(obj.get(id)); const dest = fetchDest(obj.get(id));
if (dest) { if (dest) {
return dest; return dest;
} }
@ -672,7 +718,7 @@ class Catalog {
return allDest; return allDest;
} }
} else if (obj instanceof Dict) { } else if (obj instanceof Dict) {
const dest = fetchDestination(obj.get(id)); const dest = fetchDest(obj.get(id));
if (dest) { if (dest) {
return dest; return dest;
} }
@ -1703,7 +1749,7 @@ class Catalog {
} }
if (typeof dest === "string") { if (typeof dest === "string") {
resultObj.dest = stringToPDFString(dest); resultObj.dest = stringToPDFString(dest);
} else if (Array.isArray(dest)) { } else if (isValidExplicitDest(dest)) {
resultObj.dest = dest; resultObj.dest = dest;
} }
} }

View file

@ -2933,10 +2933,9 @@ class WorkerTransport {
getPageIndex(ref) { getPageIndex(ref) {
if ( if (
typeof ref !== "object" || typeof ref !== "object" ||
ref === null || !Number.isInteger(ref?.num) ||
!Number.isInteger(ref.num) ||
ref.num < 0 || ref.num < 0 ||
!Number.isInteger(ref.gen) || !Number.isInteger(ref?.gen) ||
ref.gen < 0 ref.gen < 0
) { ) {
return Promise.reject(new Error("Invalid pageIndex request.")); return Promise.reject(new Error("Invalid pageIndex request."));

View file

@ -0,0 +1 @@
https://github.com/mozilla/pdf.js/files/15061082/unhandled_get_annotations.pdf

View file

@ -2111,6 +2111,16 @@
"lastPage": 1, "lastPage": 1,
"type": "eq" "type": "eq"
}, },
{
"id": "issue17981",
"file": "pdfs/issue17981.pdf",
"md5": "3fd6d2bdcad8ff7cc5c0575da0b70266",
"rounds": 1,
"link": true,
"lastPage": 1,
"type": "eq",
"annotations": true
},
{ {
"id": "issue9105_other", "id": "issue9105_other",
"file": "pdfs/issue9105_other.pdf", "file": "pdfs/issue9105_other.pdf",

View file

@ -454,10 +454,7 @@ class PDFLinkService {
} }
} catch {} } catch {}
if ( if (typeof dest === "string" || PDFLinkService.#isValidExplicitDest(dest)) {
typeof dest === "string" ||
PDFLinkService.#isValidExplicitDestination(dest)
) {
this.goToDestination(dest); this.goToDestination(dest);
return; return;
} }
@ -549,49 +546,44 @@ class PDFLinkService {
return this.#pagesRefCache.get(refStr) || null; return this.#pagesRefCache.get(refStr) || null;
} }
static #isValidExplicitDestination(dest) { static #isValidExplicitDest(dest) {
if (!Array.isArray(dest)) { if (!Array.isArray(dest) || dest.length < 2) {
return false; return false;
} }
const destLength = dest.length; const [page, zoom, ...args] = dest;
if (destLength < 2) {
return false;
}
const page = dest[0];
if ( if (
!( !(
typeof page === "object" && typeof page === "object" &&
Number.isInteger(page.num) && Number.isInteger(page?.num) &&
Number.isInteger(page.gen) Number.isInteger(page?.gen)
) && ) &&
!(Number.isInteger(page) && page >= 0) !Number.isInteger(page)
) { ) {
return false; return false;
} }
const zoom = dest[1]; if (!(typeof zoom === "object" && typeof zoom?.name === "string")) {
if (!(typeof zoom === "object" && typeof zoom.name === "string")) {
return false; return false;
} }
let allowNull = true; let allowNull = true;
switch (zoom.name) { switch (zoom.name) {
case "XYZ": case "XYZ":
if (destLength !== 5) { if (args.length !== 3) {
return false; return false;
} }
break; break;
case "Fit": case "Fit":
case "FitB": case "FitB":
return destLength === 2; return args.length === 0;
case "FitH": case "FitH":
case "FitBH": case "FitBH":
case "FitV": case "FitV":
case "FitBV": case "FitBV":
if (destLength !== 3) { if (args.length !== 1) {
return false; return false;
} }
break; break;
case "FitR": case "FitR":
if (destLength !== 6) { if (args.length !== 4) {
return false; return false;
} }
allowNull = false; allowNull = false;
@ -599,9 +591,8 @@ class PDFLinkService {
default: default:
return false; return false;
} }
for (let i = 2; i < destLength; i++) { for (const arg of args) {
const param = dest[i]; if (!(typeof arg === "number" || (allowNull && arg === null))) {
if (!(typeof param === "number" || (allowNull && param === null))) {
return false; return false;
} }
} }