@patrickdk77/pdf-defuse 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -11
- package/dist/crypto.d.ts +6 -2
- package/dist/crypto.js +47 -15
- package/dist/document.d.ts +207 -13
- package/dist/document.js +1571 -255
- package/dist/engine.js +12 -1
- package/dist/filters.js +59 -8
- package/dist/findings.js +3 -0
- package/dist/io.d.ts +1 -1
- package/dist/io.js +32 -9
- package/dist/parser.d.ts +92 -3
- package/dist/parser.js +188 -51
- package/dist/saslprep.d.ts +11 -0
- package/dist/saslprep.js +178 -0
- package/dist/types.d.ts +3 -0
- package/dist/types.js +3 -0
- package/dist/walker.d.ts +2 -0
- package/dist/walker.js +34 -8
- package/package.json +3 -3
package/dist/walker.js
CHANGED
|
@@ -307,6 +307,8 @@ class Walker {
|
|
|
307
307
|
inlinePages = new Map();
|
|
308
308
|
/** Objects first reached as file specifications, which stay file specifications whatever their content says. */
|
|
309
309
|
fileRole = new Set();
|
|
310
|
+
/** Page content streams, which pdf.js draws whatever their /Type says. */
|
|
311
|
+
contentRole = new Set();
|
|
310
312
|
/** Objects listed in /Annots and kept there though they are not annotations, which get the annotation rules. */
|
|
311
313
|
annotRole = new Set();
|
|
312
314
|
/** Objects that only removed content refers to, which are not unreferenced. */
|
|
@@ -829,6 +831,11 @@ class Walker {
|
|
|
829
831
|
if (slot)
|
|
830
832
|
this.inlinePages.set(slot, { index, box });
|
|
831
833
|
}
|
|
834
|
+
const contents = own?.get('Contents');
|
|
835
|
+
const parts = contents instanceof objects_1.PdfRef ? await this.doc.getObject(contents) : contents;
|
|
836
|
+
for (const c of Array.isArray(parts) ? parts : [contents])
|
|
837
|
+
if (c instanceof objects_1.PdfRef)
|
|
838
|
+
this.contentRole.add(c.num);
|
|
832
839
|
// Many pages can share one /Annots array, or inherit one. Each array is keyed by the owner and path the walk
|
|
833
840
|
// reaches it at: the last reference that leads to it, or the dictionary that holds it.
|
|
834
841
|
const annots = await read('Annots');
|
|
@@ -1057,7 +1064,8 @@ class Walker {
|
|
|
1057
1064
|
this.strippedMeta.has(num) ||
|
|
1058
1065
|
filterRefs.has(num) ||
|
|
1059
1066
|
doc.encryptRefs.has(num) ||
|
|
1060
|
-
doc.encryptRef?.num === num
|
|
1067
|
+
doc.encryptRef?.num === num ||
|
|
1068
|
+
doc.issues.objStmType.has(num))
|
|
1061
1069
|
continue;
|
|
1062
1070
|
doc.checkTime();
|
|
1063
1071
|
let obj;
|
|
@@ -1122,7 +1130,8 @@ class Walker {
|
|
|
1122
1130
|
if (!(x instanceof objects_1.PdfRef))
|
|
1123
1131
|
continue;
|
|
1124
1132
|
const s = await this.doc.getObject(x);
|
|
1125
|
-
|
|
1133
|
+
// A stream that cannot be decrypted is not written, so it adds nothing.
|
|
1134
|
+
if (s instanceof objects_1.PdfStream && classify(s) !== 'structural' && this.doc.canDecrypt(s, x.num))
|
|
1126
1135
|
total += await this.doc.plainLength(s, x.num);
|
|
1127
1136
|
}
|
|
1128
1137
|
if (arrayNum !== undefined)
|
|
@@ -1181,8 +1190,18 @@ class Walker {
|
|
|
1181
1190
|
const info = d;
|
|
1182
1191
|
if (special === 'info' && ['Author', 'Creator', 'Producer', 'Title', 'Subject', 'Keywords'].some(k => info.has(k)) && this.aggregate(types_1.PdfCategory.Metadata, types_1.PdfDetail.InfoDictionary).action === 'strip')
|
|
1183
1192
|
return undefined;
|
|
1184
|
-
// A stream decided as a contained file stays one, whether or not it says /Type /EmbeddedFile
|
|
1185
|
-
|
|
1193
|
+
// A stream decided as a contained file stays one, whether or not it says /Type /EmbeddedFile, unless it is a page's
|
|
1194
|
+
// content: only the reference that made it a contained file goes then.
|
|
1195
|
+
let kind = obj instanceof objects_1.PdfStream && this.fileDecisions.has(num) ? 'efStream' : this.kindOf(obj, num, await this.actionType(d));
|
|
1196
|
+
if (kind === 'efStream' && this.contentRole.has(num))
|
|
1197
|
+
kind = 'generic';
|
|
1198
|
+
// Nothing reads a stream encrypted with a key the password does not give, and pdf.js draws a page without it.
|
|
1199
|
+
// An attached file reports its own removal.
|
|
1200
|
+
if (obj instanceof objects_1.PdfStream && kind !== 'efStream' && kind !== 'structural' && !this.doc.canDecrypt(obj, num)) {
|
|
1201
|
+
if (this.phase === 'A' && !ctx.shadow)
|
|
1202
|
+
this.emitRemoval(types_1.PdfCategory.Encrypted, types_1.PdfDetail.NoKey, ctx.location);
|
|
1203
|
+
return undefined;
|
|
1204
|
+
}
|
|
1186
1205
|
switch (kind) {
|
|
1187
1206
|
case 'structural':
|
|
1188
1207
|
this.doc.issues.structuralRefs++;
|
|
@@ -1509,7 +1528,8 @@ class Walker {
|
|
|
1509
1528
|
for (const [k, v] of d.entries()) {
|
|
1510
1529
|
const p = `${path}/${k}`;
|
|
1511
1530
|
taken.add(k);
|
|
1512
|
-
|
|
1531
|
+
// A null /Annots or /Contents is the same as none. The writer puts null where a reference led to no object.
|
|
1532
|
+
if (k === 'Annots' && v !== null) {
|
|
1513
1533
|
let x;
|
|
1514
1534
|
if (Array.isArray(v))
|
|
1515
1535
|
x = await this.handleAnnotsArray(v, pctx, owner, p);
|
|
@@ -1534,7 +1554,7 @@ class Walker {
|
|
|
1534
1554
|
if (x !== undefined)
|
|
1535
1555
|
out.set(k, x);
|
|
1536
1556
|
}
|
|
1537
|
-
else if (k === 'Contents' && !node && !(v instanceof objects_1.PdfRef) && !Array.isArray(v)) {
|
|
1557
|
+
else if (k === 'Contents' && !node && v !== null && !(v instanceof objects_1.PdfRef) && !Array.isArray(v)) {
|
|
1538
1558
|
const f = this.emit(types_1.PdfCategory.Corrupted, types_1.PdfDetail.MalformedObject, `${pctx.location} content`);
|
|
1539
1559
|
if (f.action !== 'strip') {
|
|
1540
1560
|
const x = await this.tv(v, pctx, owner, p);
|
|
@@ -1884,7 +1904,7 @@ class Walker {
|
|
|
1884
1904
|
// Only the first 4096 characters of a label are read. Many links can share one chain and one label, so the
|
|
1885
1905
|
// outcome is kept per chain object and label.
|
|
1886
1906
|
const labels = shownLabels.map(t => t?.slice(0, 4096));
|
|
1887
|
-
const key = v instanceof objects_1.PdfRef ? `${v.num}:${(0, node_crypto_1.createHash)('
|
|
1907
|
+
const key = v instanceof objects_1.PdfRef ? `${v.num}:${(0, node_crypto_1.createHash)('sha256').update(JSON.stringify(labels)).digest('hex')}` : undefined;
|
|
1888
1908
|
let found = key === undefined ? undefined : this.labelChecks.get(key);
|
|
1889
1909
|
if (found === undefined) {
|
|
1890
1910
|
found = null;
|
|
@@ -2614,12 +2634,17 @@ class Walker {
|
|
|
2614
2634
|
const record = (keep, replacement) => {
|
|
2615
2635
|
this.fileDecisions.set(ref.num, { keep, replacement, types });
|
|
2616
2636
|
// A stream something else reached first and kept goes too.
|
|
2617
|
-
if (!keep && this.visited.has(ref.num))
|
|
2637
|
+
if (!keep && this.visited.has(ref.num) && !this.contentRole.has(ref.num))
|
|
2618
2638
|
this.dropped.add(ref.num);
|
|
2619
2639
|
return keep;
|
|
2620
2640
|
};
|
|
2621
2641
|
if (ctx.shadow)
|
|
2622
2642
|
return false;
|
|
2643
|
+
// No plugin can read a file whose key the password did not give, so it goes whatever the overrides say.
|
|
2644
|
+
if (!this.doc.canDecrypt(st, ref.num)) {
|
|
2645
|
+
this.emitRemoval(types_1.PdfCategory.EmbeddedFile, types_1.PdfDetail.NoPlugin, loc, { name: name ?? '', reason: 'encrypted' });
|
|
2646
|
+
return record(false);
|
|
2647
|
+
}
|
|
2623
2648
|
const threshold = this.options.memoryThreshold ?? 8 * 1024 * 1024;
|
|
2624
2649
|
// The first bytes alone stay in memory, however low the threshold.
|
|
2625
2650
|
const decoded = new io_1.SpillSink(this.cfg.temp, whole ? threshold : Number.POSITIVE_INFINITY);
|
|
@@ -2901,6 +2926,7 @@ class Walker {
|
|
|
2901
2926
|
this.chainSites.clear();
|
|
2902
2927
|
this.scriptFree.clear();
|
|
2903
2928
|
this.fileRole.clear();
|
|
2929
|
+
this.contentRole.clear();
|
|
2904
2930
|
this.inlinePages.clear();
|
|
2905
2931
|
return this.doc.release();
|
|
2906
2932
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@patrickdk77/pdf-defuse",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Sanitize and clean PDF files. Detects and removes JavaScript, attached files, launch actions, unsafe links and empty-password encryption, scores the risk, and writes a disarmed copy with bounded memory and no runtime dependencies.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "patrickdk77",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"LICENSE"
|
|
26
26
|
],
|
|
27
27
|
"engines": {
|
|
28
|
-
"node": ">=
|
|
28
|
+
"node": ">=18.0.0"
|
|
29
29
|
},
|
|
30
30
|
"scripts": {
|
|
31
31
|
"clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true});require('fs').rmSync('build-test',{recursive:true,force:true})\"",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"@cantoo/pdf-lib": "^2.11.1",
|
|
75
75
|
"@types/chai": "^5.2.3",
|
|
76
76
|
"@types/mocha": "^10.0.10",
|
|
77
|
-
"@types/node": "^
|
|
77
|
+
"@types/node": "^18.19.130",
|
|
78
78
|
"chai": "^6.3.0",
|
|
79
79
|
"husky": "^9.1.7",
|
|
80
80
|
"lint-staged": "^17.6.0",
|