Skip to content

Commit 7206d0a

Browse files
committed
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.
1 parent 522af26 commit 7206d0a

File tree

5 files changed

+81
-34
lines changed

5 files changed

+81
-34
lines changed

src/core/catalog.js

+54-8
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,57 @@ import { GlobalImageCache } from "./image_utils.js";
5252
import { MetadataParser } from "./metadata_parser.js";
5353
import { StructTreeRoot } from "./struct_tree.js";
5454

55-
function fetchDestination(dest) {
55+
function isValidExplicitDest(dest) {
56+
if (!Array.isArray(dest) || dest.length < 2) {
57+
return false;
58+
}
59+
const [page, zoom, ...args] = dest;
60+
if (!(page instanceof Ref) && !Number.isInteger(page)) {
61+
return false;
62+
}
63+
if (!(zoom instanceof Name)) {
64+
return false;
65+
}
66+
let allowNull = true;
67+
switch (zoom.name) {
68+
case "XYZ":
69+
if (args.length !== 3) {
70+
return false;
71+
}
72+
break;
73+
case "Fit":
74+
case "FitB":
75+
return args.length === 0;
76+
case "FitH":
77+
case "FitBH":
78+
case "FitV":
79+
case "FitBV":
80+
if (args.length !== 1) {
81+
return false;
82+
}
83+
break;
84+
case "FitR":
85+
if (args.length !== 4) {
86+
return false;
87+
}
88+
allowNull = false;
89+
break;
90+
default:
91+
return false;
92+
}
93+
for (const arg of args) {
94+
if (!(typeof arg === "number" || (allowNull && arg === null))) {
95+
return false;
96+
}
97+
}
98+
return true;
99+
}
100+
101+
function fetchDest(dest) {
56102
if (dest instanceof Dict) {
57103
dest = dest.get("D");
58104
}
59-
return Array.isArray(dest) ? dest : null;
105+
return isValidExplicitDest(dest) ? dest : null;
60106
}
61107

62108
function fetchRemoteDest(action) {
@@ -67,7 +113,7 @@ function fetchRemoteDest(action) {
67113
}
68114
if (typeof dest === "string") {
69115
return stringToPDFString(dest);
70-
} else if (Array.isArray(dest)) {
116+
} else if (isValidExplicitDest(dest)) {
71117
return JSON.stringify(dest);
72118
}
73119
}
@@ -641,14 +687,14 @@ class Catalog {
641687
dests = Object.create(null);
642688
if (obj instanceof NameTree) {
643689
for (const [key, value] of obj.getAll()) {
644-
const dest = fetchDestination(value);
690+
const dest = fetchDest(value);
645691
if (dest) {
646692
dests[stringToPDFString(key)] = dest;
647693
}
648694
}
649695
} else if (obj instanceof Dict) {
650696
obj.forEach(function (key, value) {
651-
const dest = fetchDestination(value);
697+
const dest = fetchDest(value);
652698
if (dest) {
653699
dests[key] = dest;
654700
}
@@ -660,7 +706,7 @@ class Catalog {
660706
getDestination(id) {
661707
const obj = this._readDests();
662708
if (obj instanceof NameTree) {
663-
const dest = fetchDestination(obj.get(id));
709+
const dest = fetchDest(obj.get(id));
664710
if (dest) {
665711
return dest;
666712
}
@@ -672,7 +718,7 @@ class Catalog {
672718
return allDest;
673719
}
674720
} else if (obj instanceof Dict) {
675-
const dest = fetchDestination(obj.get(id));
721+
const dest = fetchDest(obj.get(id));
676722
if (dest) {
677723
return dest;
678724
}
@@ -1703,7 +1749,7 @@ class Catalog {
17031749
}
17041750
if (typeof dest === "string") {
17051751
resultObj.dest = stringToPDFString(dest);
1706-
} else if (Array.isArray(dest)) {
1752+
} else if (isValidExplicitDest(dest)) {
17071753
resultObj.dest = dest;
17081754
}
17091755
}

src/display/api.js

+2-3
Original file line numberDiff line numberDiff line change
@@ -2933,10 +2933,9 @@ class WorkerTransport {
29332933
getPageIndex(ref) {
29342934
if (
29352935
typeof ref !== "object" ||
2936-
ref === null ||
2937-
!Number.isInteger(ref.num) ||
2936+
!Number.isInteger(ref?.num) ||
29382937
ref.num < 0 ||
2939-
!Number.isInteger(ref.gen) ||
2938+
!Number.isInteger(ref?.gen) ||
29402939
ref.gen < 0
29412940
) {
29422941
return Promise.reject(new Error("Invalid pageIndex request."));

test/pdfs/issue17981.pdf.link

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
https://github.com/mozilla/pdf.js/files/15061082/unhandled_get_annotations.pdf

test/test_manifest.json

+10
Original file line numberDiff line numberDiff line change
@@ -2111,6 +2111,16 @@
21112111
"lastPage": 1,
21122112
"type": "eq"
21132113
},
2114+
{
2115+
"id": "issue17981",
2116+
"file": "pdfs/issue17981.pdf",
2117+
"md5": "3fd6d2bdcad8ff7cc5c0575da0b70266",
2118+
"rounds": 1,
2119+
"link": true,
2120+
"lastPage": 1,
2121+
"type": "eq",
2122+
"annotations": true
2123+
},
21142124
{
21152125
"id": "issue9105_other",
21162126
"file": "pdfs/issue9105_other.pdf",

web/pdf_link_service.js

+14-23
Original file line numberDiff line numberDiff line change
@@ -454,10 +454,7 @@ class PDFLinkService {
454454
}
455455
} catch {}
456456

457-
if (
458-
typeof dest === "string" ||
459-
PDFLinkService.#isValidExplicitDestination(dest)
460-
) {
457+
if (typeof dest === "string" || PDFLinkService.#isValidExplicitDest(dest)) {
461458
this.goToDestination(dest);
462459
return;
463460
}
@@ -549,59 +546,53 @@ class PDFLinkService {
549546
return this.#pagesRefCache.get(refStr) || null;
550547
}
551548

552-
static #isValidExplicitDestination(dest) {
553-
if (!Array.isArray(dest)) {
554-
return false;
555-
}
556-
const destLength = dest.length;
557-
if (destLength < 2) {
549+
static #isValidExplicitDest(dest) {
550+
if (!Array.isArray(dest) || dest.length < 2) {
558551
return false;
559552
}
560-
const page = dest[0];
553+
const [page, zoom, ...args] = dest;
561554
if (
562555
!(
563556
typeof page === "object" &&
564-
Number.isInteger(page.num) &&
565-
Number.isInteger(page.gen)
557+
Number.isInteger(page?.num) &&
558+
Number.isInteger(page?.gen)
566559
) &&
567-
!(Number.isInteger(page) && page >= 0)
560+
!Number.isInteger(page)
568561
) {
569562
return false;
570563
}
571-
const zoom = dest[1];
572-
if (!(typeof zoom === "object" && typeof zoom.name === "string")) {
564+
if (!(typeof zoom === "object" && typeof zoom?.name === "string")) {
573565
return false;
574566
}
575567
let allowNull = true;
576568
switch (zoom.name) {
577569
case "XYZ":
578-
if (destLength !== 5) {
570+
if (args.length !== 3) {
579571
return false;
580572
}
581573
break;
582574
case "Fit":
583575
case "FitB":
584-
return destLength === 2;
576+
return args.length === 0;
585577
case "FitH":
586578
case "FitBH":
587579
case "FitV":
588580
case "FitBV":
589-
if (destLength !== 3) {
581+
if (args.length !== 1) {
590582
return false;
591583
}
592584
break;
593585
case "FitR":
594-
if (destLength !== 6) {
586+
if (args.length !== 4) {
595587
return false;
596588
}
597589
allowNull = false;
598590
break;
599591
default:
600592
return false;
601593
}
602-
for (let i = 2; i < destLength; i++) {
603-
const param = dest[i];
604-
if (!(typeof param === "number" || (allowNull && param === null))) {
594+
for (const arg of args) {
595+
if (!(typeof arg === "number" || (allowNull && arg === null))) {
605596
return false;
606597
}
607598
}

0 commit comments

Comments
 (0)