@patrickdk77/pdf-defuse 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +323 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +390 -0
- package/dist/crypto.d.ts +51 -0
- package/dist/crypto.js +404 -0
- package/dist/data-plugins.d.ts +28 -0
- package/dist/data-plugins.js +525 -0
- package/dist/document.d.ts +191 -0
- package/dist/document.js +1403 -0
- package/dist/engine.d.ts +16 -0
- package/dist/engine.js +392 -0
- package/dist/filters.d.ts +27 -0
- package/dist/filters.js +616 -0
- package/dist/findings.d.ts +24 -0
- package/dist/findings.js +183 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +42 -0
- package/dist/index.mjs +3 -0
- package/dist/io.d.ts +81 -0
- package/dist/io.js +414 -0
- package/dist/objects.d.ts +52 -0
- package/dist/objects.js +178 -0
- package/dist/parser.d.ts +70 -0
- package/dist/parser.js +534 -0
- package/dist/plugins.d.ts +13 -0
- package/dist/plugins.js +56 -0
- package/dist/score.d.ts +17 -0
- package/dist/score.js +63 -0
- package/dist/sniff.d.ts +15 -0
- package/dist/sniff.js +118 -0
- package/dist/types.d.ts +294 -0
- package/dist/types.js +143 -0
- package/dist/uri.d.ts +29 -0
- package/dist/uri.js +150 -0
- package/dist/walker.d.ts +302 -0
- package/dist/walker.js +2914 -0
- package/dist/writer.d.ts +6 -0
- package/dist/writer.js +112 -0
- package/package.json +86 -4
package/dist/score.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.COMBINATIONS = exports.DEFAULT_BANDS = void 0;
|
|
4
|
+
exports.weightOf = weightOf;
|
|
5
|
+
exports.scoreFindings = scoreFindings;
|
|
6
|
+
exports.riskFor = riskFor;
|
|
7
|
+
const findings_1 = require("./findings");
|
|
8
|
+
const types_1 = require("./types");
|
|
9
|
+
exports.DEFAULT_BANDS = { low: 1, medium: 25, high: 50, critical: 80 };
|
|
10
|
+
exports.COMBINATIONS = [
|
|
11
|
+
{
|
|
12
|
+
name: 'auto-run with an outside action',
|
|
13
|
+
weight: 20,
|
|
14
|
+
test: has => (has(types_1.PdfCategory.JavaScript, types_1.PdfDetail.OpenAction) || has(types_1.PdfCategory.JavaScript, types_1.PdfDetail.Document) || has(types_1.PdfCategory.Action, types_1.PdfDetail.Triggered) || has(types_1.PdfCategory.JavaScript, types_1.PdfDetail.Page)) &&
|
|
15
|
+
(has(types_1.PdfCategory.Action, types_1.PdfDetail.Launch) || has(types_1.PdfCategory.Action, types_1.PdfDetail.RemoteGoto) || has(types_1.PdfCategory.Action, types_1.PdfDetail.EmbeddedGoto) || has(types_1.PdfCategory.Action, types_1.PdfDetail.SubmitForm)),
|
|
16
|
+
},
|
|
17
|
+
{ name: 'JavaScript with escaped keywords', weight: 20, test: has => has(types_1.PdfCategory.JavaScript) && has(types_1.PdfCategory.Structure, types_1.PdfDetail.EscapedNames) },
|
|
18
|
+
{ name: 'JavaScript with an embedded file', weight: 15, test: has => has(types_1.PdfCategory.JavaScript) && has(types_1.PdfCategory.EmbeddedFile) },
|
|
19
|
+
{
|
|
20
|
+
name: 'JavaScript with empty-password encryption or JBIG2',
|
|
21
|
+
weight: 10,
|
|
22
|
+
test: has => has(types_1.PdfCategory.JavaScript) && (has(types_1.PdfCategory.Encrypted, types_1.PdfDetail.EmptyPassword) || has(types_1.PdfCategory.Content, types_1.PdfDetail.Jbig2Image)),
|
|
23
|
+
},
|
|
24
|
+
];
|
|
25
|
+
/** The caller's weights first, then the built-in table. A kind from another package scores at the weight it carries. */
|
|
26
|
+
function weightOf(f, options) {
|
|
27
|
+
const w = options.scoreWeights ?? [];
|
|
28
|
+
const exact = w.find(x => x.category === f.category && x.detail === f.detail);
|
|
29
|
+
if (exact)
|
|
30
|
+
return exact.weight;
|
|
31
|
+
const cat = w.find(x => x.category === f.category && x.detail === undefined);
|
|
32
|
+
if (cat)
|
|
33
|
+
return cat.weight;
|
|
34
|
+
return (0, findings_1.findingSpec)(f.category, f.detail)?.weight ?? f.weight ?? 0;
|
|
35
|
+
}
|
|
36
|
+
/** Highest weight in full, a quarter of each other kind's weight, plus combinations, capped at 100. */
|
|
37
|
+
function scoreFindings(findings, options) {
|
|
38
|
+
// Each kind of finding counts once, so twenty copies of one minor finding do not add up to a critical file.
|
|
39
|
+
const byKind = new Map();
|
|
40
|
+
for (const f of findings) {
|
|
41
|
+
const k = `${f.category}/${f.detail}`;
|
|
42
|
+
byKind.set(k, Math.max(byKind.get(k) ?? 0, weightOf(f, options)));
|
|
43
|
+
}
|
|
44
|
+
const weights = [...byKind.values()].sort((a, b) => b - a);
|
|
45
|
+
let score = weights.length ? weights[0] + weights.slice(1).reduce((s, w) => s + w / 4, 0) : 0;
|
|
46
|
+
const has = (c, d) => findings.some(f => f.category === c && (d === undefined || f.detail === d) && weightOf(f, options) > 0);
|
|
47
|
+
for (const combo of exports.COMBINATIONS)
|
|
48
|
+
if (combo.test(has))
|
|
49
|
+
score += combo.weight;
|
|
50
|
+
score = Math.min(100, Math.round(score));
|
|
51
|
+
return { score, risk: riskFor(score, options.scoreBands ?? exports.DEFAULT_BANDS) };
|
|
52
|
+
}
|
|
53
|
+
function riskFor(score, bands) {
|
|
54
|
+
if (score >= bands.critical)
|
|
55
|
+
return types_1.PdfRisk.Critical;
|
|
56
|
+
if (score >= bands.high)
|
|
57
|
+
return types_1.PdfRisk.High;
|
|
58
|
+
if (score >= bands.medium)
|
|
59
|
+
return types_1.PdfRisk.Medium;
|
|
60
|
+
if (score >= bands.low)
|
|
61
|
+
return types_1.PdfRisk.Low;
|
|
62
|
+
return types_1.PdfRisk.None;
|
|
63
|
+
}
|
package/dist/sniff.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Identifies a file type from its first bytes. Returns a MIME type or undefined. */
|
|
2
|
+
export declare function sniffType(head: Uint8Array): string | undefined;
|
|
3
|
+
/** Maps MIME types that share a container format, and common aliases of one type, onto one family. */
|
|
4
|
+
export declare function family(mime: string): string;
|
|
5
|
+
/**
|
|
6
|
+
* A file name's extension, lowercased. Windows drops trailing dots and spaces when it saves a file, so the extension
|
|
7
|
+
* of "page.html." is html.
|
|
8
|
+
*/
|
|
9
|
+
export declare function extensionOf(name?: string): string | undefined;
|
|
10
|
+
export declare function typeFromName(name?: string): string | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* True when any two of the declared type, the name's extension and the sniffed content contradict each other, or
|
|
13
|
+
* when the declared type or the name claims a type sniffType recognizes and the content matched no signature.
|
|
14
|
+
*/
|
|
15
|
+
export declare function typesDisagree(declared: string | undefined, name: string | undefined, sniffed: string | undefined): boolean;
|
package/dist/sniff.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sniffType = sniffType;
|
|
4
|
+
exports.family = family;
|
|
5
|
+
exports.extensionOf = extensionOf;
|
|
6
|
+
exports.typeFromName = typeFromName;
|
|
7
|
+
exports.typesDisagree = typesDisagree;
|
|
8
|
+
/** Identifies a file type from its first bytes. Returns a MIME type or undefined. */
|
|
9
|
+
function sniffType(head) {
|
|
10
|
+
const b = Buffer.from(head.buffer, head.byteOffset, head.byteLength);
|
|
11
|
+
// PDF readers accept the header anywhere in the first 1024 bytes.
|
|
12
|
+
const s = b.subarray(0, 1024).toString('latin1');
|
|
13
|
+
if (s.includes('%PDF-'))
|
|
14
|
+
return 'application/pdf';
|
|
15
|
+
if (b[0] === 0x50 && b[1] === 0x4b && (b[2] === 0x03 || b[2] === 0x05 || b[2] === 0x07))
|
|
16
|
+
return 'application/zip';
|
|
17
|
+
if (b[0] === 0x89 && s.startsWith('\x89PNG'))
|
|
18
|
+
return 'image/png';
|
|
19
|
+
if (b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff)
|
|
20
|
+
return 'image/jpeg';
|
|
21
|
+
if (s.startsWith('GIF87a') || s.startsWith('GIF89a'))
|
|
22
|
+
return 'image/gif';
|
|
23
|
+
if (b[0] === 0xd0 && b[1] === 0xcf && b[2] === 0x11 && b[3] === 0xe0)
|
|
24
|
+
return 'application/x-ole-storage';
|
|
25
|
+
if (s.startsWith('MZ'))
|
|
26
|
+
return 'application/x-msdownload';
|
|
27
|
+
if (s.startsWith('\x7fELF'))
|
|
28
|
+
return 'application/x-executable';
|
|
29
|
+
const text = s.replace(/^\xef\xbb\xbf/, '').trimStart();
|
|
30
|
+
if (/^<svg[\s>/]/i.test(text) || (/^<\?xml/i.test(text) && /<svg[\s>/]/i.test(s)))
|
|
31
|
+
return 'image/svg+xml';
|
|
32
|
+
if (/^<\?xml/i.test(text))
|
|
33
|
+
return 'application/xml';
|
|
34
|
+
if (/^<!doctype html|^<html[\s>]/i.test(text))
|
|
35
|
+
return 'text/html';
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
const EXT = {
|
|
39
|
+
pdf: 'application/pdf',
|
|
40
|
+
zip: 'application/zip',
|
|
41
|
+
docx: 'application/zip',
|
|
42
|
+
xlsx: 'application/zip',
|
|
43
|
+
pptx: 'application/zip',
|
|
44
|
+
odt: 'application/zip',
|
|
45
|
+
ods: 'application/zip',
|
|
46
|
+
odp: 'application/zip',
|
|
47
|
+
epub: 'application/zip',
|
|
48
|
+
jar: 'application/zip',
|
|
49
|
+
png: 'image/png',
|
|
50
|
+
jpg: 'image/jpeg',
|
|
51
|
+
jpeg: 'image/jpeg',
|
|
52
|
+
gif: 'image/gif',
|
|
53
|
+
svg: 'image/svg+xml',
|
|
54
|
+
xml: 'application/xml',
|
|
55
|
+
html: 'text/html',
|
|
56
|
+
htm: 'text/html',
|
|
57
|
+
doc: 'application/x-ole-storage',
|
|
58
|
+
xls: 'application/x-ole-storage',
|
|
59
|
+
ppt: 'application/x-ole-storage',
|
|
60
|
+
msg: 'application/x-ole-storage',
|
|
61
|
+
exe: 'application/x-msdownload',
|
|
62
|
+
dll: 'application/x-msdownload',
|
|
63
|
+
csv: 'text/csv',
|
|
64
|
+
tsv: 'text/tab-separated-values',
|
|
65
|
+
tab: 'text/tab-separated-values',
|
|
66
|
+
json: 'application/json',
|
|
67
|
+
};
|
|
68
|
+
/** Maps MIME types that share a container format, and common aliases of one type, onto one family. */
|
|
69
|
+
function family(mime) {
|
|
70
|
+
const m = mime.toLowerCase().split(';')[0].trim();
|
|
71
|
+
if (m === 'image/jpg' || m === 'image/pjpeg')
|
|
72
|
+
return 'image/jpeg';
|
|
73
|
+
if (m === 'image/x-png')
|
|
74
|
+
return 'image/png';
|
|
75
|
+
if (m === 'application/x-pdf')
|
|
76
|
+
return 'application/pdf';
|
|
77
|
+
if (m === 'application/zip' ||
|
|
78
|
+
m === 'application/x-zip-compressed' ||
|
|
79
|
+
m === 'application/x-zip' ||
|
|
80
|
+
m.includes('openxmlformats') ||
|
|
81
|
+
m.includes('opendocument') ||
|
|
82
|
+
m === 'application/epub+zip' ||
|
|
83
|
+
m === 'application/java-archive')
|
|
84
|
+
return 'zip';
|
|
85
|
+
if (m === 'image/svg+xml')
|
|
86
|
+
return 'svg';
|
|
87
|
+
if (m === 'application/xml' || m === 'text/xml')
|
|
88
|
+
return 'xml';
|
|
89
|
+
if (m === 'application/msword' || m === 'application/vnd.ms-excel' || m === 'application/vnd.ms-powerpoint' || m === 'application/x-ole-storage' || m === 'application/vnd.ms-outlook')
|
|
90
|
+
return 'ole';
|
|
91
|
+
return m;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* A file name's extension, lowercased. Windows drops trailing dots and spaces when it saves a file, so the extension
|
|
95
|
+
* of "page.html." is html.
|
|
96
|
+
*/
|
|
97
|
+
function extensionOf(name) {
|
|
98
|
+
return /\.([^./\\]+)$/.exec((name ?? '').replace(/[. ]+$/, ''))?.[1].toLowerCase();
|
|
99
|
+
}
|
|
100
|
+
function typeFromName(name) {
|
|
101
|
+
const ext = extensionOf(name);
|
|
102
|
+
// Only the table's own entries: "x.constructor" names no type.
|
|
103
|
+
return ext !== undefined && Object.hasOwn(EXT, ext) ? EXT[ext] : undefined;
|
|
104
|
+
}
|
|
105
|
+
/** Families sniffType has a signature for. Content that claims one of them and matches no signature is not what it claims. */
|
|
106
|
+
const SNIFFABLE = new Set(['application/pdf', 'zip', 'image/png', 'image/jpeg', 'image/gif', 'ole', 'application/x-msdownload', 'application/x-executable', 'svg', 'xml', 'text/html']);
|
|
107
|
+
/**
|
|
108
|
+
* True when any two of the declared type, the name's extension and the sniffed content contradict each other, or
|
|
109
|
+
* when the declared type or the name claims a type sniffType recognizes and the content matched no signature.
|
|
110
|
+
*/
|
|
111
|
+
function typesDisagree(declared, name, sniffed) {
|
|
112
|
+
const claimed = [declared, typeFromName(name)].filter((t) => t !== undefined && t !== '' && family(t) !== 'application/octet-stream').map(family);
|
|
113
|
+
const all = sniffed ? [...claimed, family(sniffed)] : claimed;
|
|
114
|
+
const same = (a, b) => a === b || (a === 'svg' && b === 'xml') || (a === 'xml' && b === 'svg');
|
|
115
|
+
if (all.some(a => all.some(b => !same(a, b))))
|
|
116
|
+
return true;
|
|
117
|
+
return !sniffed && claimed.some(f => SNIFFABLE.has(f));
|
|
118
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/** High-level kind of a finding. */
|
|
2
|
+
export declare enum PdfCategory {
|
|
3
|
+
Encrypted = "ENCRYPTED",
|
|
4
|
+
Corrupted = "CORRUPTED",
|
|
5
|
+
JavaScript = "JAVASCRIPT",
|
|
6
|
+
Action = "ACTION",
|
|
7
|
+
Link = "LINK",
|
|
8
|
+
EmbeddedFile = "EMBEDDED_FILE",
|
|
9
|
+
Media = "MEDIA",
|
|
10
|
+
Annotation = "ANNOTATION",
|
|
11
|
+
Form = "FORM",
|
|
12
|
+
Signature = "SIGNATURE",
|
|
13
|
+
Structure = "STRUCTURE",
|
|
14
|
+
Content = "CONTENT",
|
|
15
|
+
Metadata = "METADATA",
|
|
16
|
+
Limit = "LIMIT",
|
|
17
|
+
Processing = "PROCESSING"
|
|
18
|
+
}
|
|
19
|
+
/** Exact variant of a finding. Some details appear under more than one category. */
|
|
20
|
+
export declare enum PdfDetail {
|
|
21
|
+
EmptyPassword = "EMPTY_PASSWORD",
|
|
22
|
+
UserPassword = "USER_PASSWORD",
|
|
23
|
+
OwnerPassword = "OWNER_PASSWORD",
|
|
24
|
+
PasswordRequired = "PASSWORD_REQUIRED",
|
|
25
|
+
CertificateHandler = "CERTIFICATE_HANDLER",
|
|
26
|
+
UnknownHandler = "UNKNOWN_HANDLER",
|
|
27
|
+
UnknownCryptFilter = "UNKNOWN_CRYPT_FILTER",
|
|
28
|
+
Rc4_40 = "RC4_40",
|
|
29
|
+
Rc4_128 = "RC4_128",
|
|
30
|
+
Rc4Other = "RC4_OTHER",
|
|
31
|
+
Aes128 = "AES_128",
|
|
32
|
+
Aes256 = "AES_256",
|
|
33
|
+
MetadataUnencrypted = "METADATA_UNENCRYPTED",
|
|
34
|
+
Unparseable = "UNPARSEABLE",
|
|
35
|
+
Truncated = "TRUNCATED",
|
|
36
|
+
XrefRebuilt = "XREF_REBUILT",
|
|
37
|
+
MalformedObject = "MALFORMED_OBJECT",
|
|
38
|
+
StreamLengthWrong = "STREAM_LENGTH_WRONG",
|
|
39
|
+
LeadingBytes = "LEADING_BYTES",
|
|
40
|
+
TrailingBytes = "TRAILING_BYTES",
|
|
41
|
+
Document = "DOCUMENT",
|
|
42
|
+
OpenAction = "OPEN_ACTION",
|
|
43
|
+
Page = "PAGE",
|
|
44
|
+
Field = "FIELD",
|
|
45
|
+
Annotation = "ANNOTATION",
|
|
46
|
+
Link = "LINK",
|
|
47
|
+
Bookmark = "BOOKMARK",
|
|
48
|
+
Chained = "CHAINED",
|
|
49
|
+
Rendition = "RENDITION",
|
|
50
|
+
Url = "URL",
|
|
51
|
+
Unattached = "UNATTACHED",
|
|
52
|
+
PluginPassed = "PLUGIN_PASSED",
|
|
53
|
+
PluginScrubbed = "PLUGIN_SCRUBBED",
|
|
54
|
+
PluginFailed = "PLUGIN_FAILED",
|
|
55
|
+
PluginRemoved = "PLUGIN_REMOVED",
|
|
56
|
+
Launch = "LAUNCH",
|
|
57
|
+
RemoteGoto = "REMOTE_GOTO",
|
|
58
|
+
EmbeddedGoto = "EMBEDDED_GOTO",
|
|
59
|
+
SubmitForm = "SUBMIT_FORM",
|
|
60
|
+
ImportData = "IMPORT_DATA",
|
|
61
|
+
Hide = "HIDE",
|
|
62
|
+
SetLayerState = "SET_LAYER_STATE",
|
|
63
|
+
Named = "NAMED",
|
|
64
|
+
Triggered = "TRIGGERED",
|
|
65
|
+
Unknown = "UNKNOWN",
|
|
66
|
+
Safe = "SAFE",
|
|
67
|
+
FileUrl = "FILE_URL",
|
|
68
|
+
NetworkPath = "NETWORK_PATH",
|
|
69
|
+
DataUrl = "DATA_URL",
|
|
70
|
+
OtherScheme = "OTHER_SCHEME",
|
|
71
|
+
Credentials = "CREDENTIALS",
|
|
72
|
+
IpHost = "IP_HOST",
|
|
73
|
+
LookalikeHost = "LOOKALIKE_HOST",
|
|
74
|
+
EncodedHost = "ENCODED_HOST",
|
|
75
|
+
TextMismatch = "TEXT_MISMATCH",
|
|
76
|
+
FullPage = "FULL_PAGE",
|
|
77
|
+
Relative = "RELATIVE",
|
|
78
|
+
NoPlugin = "NO_PLUGIN",
|
|
79
|
+
TypeMismatch = "TYPE_MISMATCH",
|
|
80
|
+
Portfolio = "PORTFOLIO",
|
|
81
|
+
Movie = "MOVIE",
|
|
82
|
+
Sound = "SOUND",
|
|
83
|
+
Screen = "SCREEN",
|
|
84
|
+
RichMedia = "RICH_MEDIA",
|
|
85
|
+
ThreeD = "THREE_D",
|
|
86
|
+
Slideshow = "SLIDESHOW",
|
|
87
|
+
UnknownSubtype = "UNKNOWN_SUBTYPE",
|
|
88
|
+
Fields = "FIELDS",
|
|
89
|
+
Xfa = "XFA",
|
|
90
|
+
CalculationOrder = "CALCULATION_ORDER",
|
|
91
|
+
Signed = "SIGNED",
|
|
92
|
+
Certified = "CERTIFIED",
|
|
93
|
+
UsageRights = "USAGE_RIGHTS",
|
|
94
|
+
IncrementalUpdates = "INCREMENTAL_UPDATES",
|
|
95
|
+
UnreferencedObjects = "UNREFERENCED_OBJECTS",
|
|
96
|
+
VersionUpgraded = "VERSION_UPGRADED",
|
|
97
|
+
EscapedNames = "ESCAPED_NAMES",
|
|
98
|
+
ShadowedObjects = "SHADOWED_OBJECTS",
|
|
99
|
+
Jbig2Image = "JBIG2_IMAGE",
|
|
100
|
+
JpxImage = "JPX_IMAGE",
|
|
101
|
+
Type3Font = "TYPE3_FONT",
|
|
102
|
+
InfoDictionary = "INFO_DICTIONARY",
|
|
103
|
+
Xmp = "XMP",
|
|
104
|
+
Stripped = "STRIPPED",
|
|
105
|
+
FileSize = "FILE_SIZE",
|
|
106
|
+
ObjectCount = "OBJECT_COUNT",
|
|
107
|
+
DecompressedSize = "DECOMPRESSED_SIZE",
|
|
108
|
+
NestingDepth = "NESTING_DEPTH",
|
|
109
|
+
Time = "TIME",
|
|
110
|
+
MemoryFallback = "MEMORY_FALLBACK",
|
|
111
|
+
VerificationFailed = "VERIFICATION_FAILED",
|
|
112
|
+
ContentRemoved = "CONTENT_REMOVED"
|
|
113
|
+
}
|
|
114
|
+
export type PdfFindingAction = 'reject' | 'strip' | 'info';
|
|
115
|
+
/**
|
|
116
|
+
* A finding as any package of the defuse family reports it. Category and detail are plain strings, since the string
|
|
117
|
+
* enums of two packages never match each other.
|
|
118
|
+
*/
|
|
119
|
+
export interface DefuseFinding {
|
|
120
|
+
category: string;
|
|
121
|
+
detail: string;
|
|
122
|
+
/** Plain sentence, safe to show the uploader. */
|
|
123
|
+
description: string;
|
|
124
|
+
/** Default action after any caller override. */
|
|
125
|
+
action: 'reject' | 'strip' | 'info';
|
|
126
|
+
/** Where in the file, e.g. "page 3, annotation 2". */
|
|
127
|
+
location?: string;
|
|
128
|
+
/** Extra values: the URI, key length, plugin name, counts. */
|
|
129
|
+
data?: Record<string, string | number>;
|
|
130
|
+
/**
|
|
131
|
+
* Set when the finding comes from inside a contained file, such as a PDF attached to the PDF: the file's
|
|
132
|
+
* name, with " > " between nesting levels. `location` then starts with where that file sits.
|
|
133
|
+
*/
|
|
134
|
+
attachment?: string;
|
|
135
|
+
/** How much the finding adds to a score, set by the package that made it. */
|
|
136
|
+
weight?: number;
|
|
137
|
+
}
|
|
138
|
+
/** A finding pdf-defuse makes itself. */
|
|
139
|
+
export interface PdfFinding extends DefuseFinding {
|
|
140
|
+
category: PdfCategory;
|
|
141
|
+
detail: PdfDetail;
|
|
142
|
+
}
|
|
143
|
+
export declare enum PdfRisk {
|
|
144
|
+
None = "NONE",
|
|
145
|
+
Low = "LOW",
|
|
146
|
+
Medium = "MEDIUM",
|
|
147
|
+
High = "HIGH",
|
|
148
|
+
Critical = "CRITICAL",
|
|
149
|
+
/** The file could not be inspected: wrong password, unparseable, or a limit stopped the run. */
|
|
150
|
+
Unknown = "UNKNOWN"
|
|
151
|
+
}
|
|
152
|
+
export interface PdfInspection {
|
|
153
|
+
status: 'clean' | 'strippable' | 'rejected';
|
|
154
|
+
/** 0 to 100, null when risk is UNKNOWN. */
|
|
155
|
+
score: number | null;
|
|
156
|
+
risk: PdfRisk;
|
|
157
|
+
findings: DefuseFinding[];
|
|
158
|
+
version: string;
|
|
159
|
+
pages?: number;
|
|
160
|
+
}
|
|
161
|
+
export interface PdfDisarmResult {
|
|
162
|
+
status: 'clean' | 'defused' | 'rejected';
|
|
163
|
+
/** The uploaded file and its score. */
|
|
164
|
+
before: PdfInspection;
|
|
165
|
+
/** The output and its remaining score. Absent when rejected. */
|
|
166
|
+
after?: PdfInspection;
|
|
167
|
+
/** Findings whose content was removed. */
|
|
168
|
+
removed: DefuseFinding[];
|
|
169
|
+
}
|
|
170
|
+
/** Random-access input. A buffer, a local file and an S3 ranged GetObject all fit. */
|
|
171
|
+
export interface ByteSource {
|
|
172
|
+
size(): Promise<number>;
|
|
173
|
+
read(offset: number, length: number): Promise<Uint8Array>;
|
|
174
|
+
close?(): Promise<void>;
|
|
175
|
+
}
|
|
176
|
+
/** Append-only output. Never seeks. */
|
|
177
|
+
export interface ByteSink {
|
|
178
|
+
write(chunk: Uint8Array): Promise<void>;
|
|
179
|
+
close(): Promise<void>;
|
|
180
|
+
/** Called instead of close() when copying to the sink fails, so a partial output is not taken for a whole one. */
|
|
181
|
+
abort?(error: unknown): Promise<void>;
|
|
182
|
+
}
|
|
183
|
+
export interface PdfLimits {
|
|
184
|
+
/** Bytes. */
|
|
185
|
+
fileSize?: number;
|
|
186
|
+
/** Live objects in the file. */
|
|
187
|
+
objects?: number;
|
|
188
|
+
/** Decoded size of any one stream. */
|
|
189
|
+
decompressedBytes?: number;
|
|
190
|
+
/**
|
|
191
|
+
* The deepest a PDF may sit in the upload. The upload is at depth 0 and a file inside it at 1, and files of every
|
|
192
|
+
* format in between count.
|
|
193
|
+
*/
|
|
194
|
+
nestingDepth?: number;
|
|
195
|
+
timeMs?: number;
|
|
196
|
+
}
|
|
197
|
+
/** What a contained file plugin learns of the run, with nothing PDF-specific, so any package of the family can make one. */
|
|
198
|
+
export interface PluginContext {
|
|
199
|
+
/** The depth of the file itself: 0 for the upload, 1 for a file inside it, and so on, whatever the formats. */
|
|
200
|
+
depth: number;
|
|
201
|
+
/** When `limits.timeMs` is set, the Date.now() value the whole run must finish by. Nested runs share it. */
|
|
202
|
+
deadline?: number;
|
|
203
|
+
}
|
|
204
|
+
export interface ContainedFile {
|
|
205
|
+
name?: string;
|
|
206
|
+
/** The MIME type the container declares for the file, such as the /Subtype of a PDF's embedded file stream. */
|
|
207
|
+
declaredType?: string;
|
|
208
|
+
/** From the first bytes. */
|
|
209
|
+
sniffedType?: string;
|
|
210
|
+
/** Decoded size. */
|
|
211
|
+
size: number;
|
|
212
|
+
location: string;
|
|
213
|
+
/** The depth of the file itself: 0 for the upload, 1 for a file inside it, and so on, whatever the formats. */
|
|
214
|
+
depth: number;
|
|
215
|
+
/** Decoded and decrypted bytes. */
|
|
216
|
+
source: ByteSource;
|
|
217
|
+
}
|
|
218
|
+
/** What a file plugin did, plus anything it found inside the file. */
|
|
219
|
+
export interface ContainedFileResult {
|
|
220
|
+
result: 'scrubbed' | 'passed' | 'removed';
|
|
221
|
+
/** Findings inside the file as it arrived. They go into the report of the file that contains it. */
|
|
222
|
+
findings?: DefuseFinding[];
|
|
223
|
+
/** Findings inside the bytes the plugin kept. They go into the report of the output. */
|
|
224
|
+
outputFindings?: DefuseFinding[];
|
|
225
|
+
}
|
|
226
|
+
export interface ContainedFilePlugin {
|
|
227
|
+
kind: 'file';
|
|
228
|
+
name: string;
|
|
229
|
+
accepts(file: ContainedFile): boolean | Promise<boolean>;
|
|
230
|
+
/** Writes the bytes to keep. Returning 'removed' drops the file after all. */
|
|
231
|
+
process(file: ContainedFile, sink: ByteSink, context: PluginContext): Promise<'scrubbed' | 'passed' | 'removed' | ContainedFileResult>;
|
|
232
|
+
}
|
|
233
|
+
export interface ContainedScript {
|
|
234
|
+
text: string;
|
|
235
|
+
/** "document", "open", "page-open", "field-format", "link", "bookmark", ... */
|
|
236
|
+
trigger: string;
|
|
237
|
+
location: string;
|
|
238
|
+
fieldName?: string;
|
|
239
|
+
/** Names of document-level scripts a plugin kept. */
|
|
240
|
+
keptDocumentScripts: string[];
|
|
241
|
+
}
|
|
242
|
+
export interface ScriptPlugin {
|
|
243
|
+
kind: 'script';
|
|
244
|
+
name: string;
|
|
245
|
+
accepts(script: ContainedScript): boolean | Promise<boolean>;
|
|
246
|
+
process(script: ContainedScript): Promise<{
|
|
247
|
+
result: 'scrubbed' | 'passed' | 'removed';
|
|
248
|
+
text?: string;
|
|
249
|
+
}>;
|
|
250
|
+
}
|
|
251
|
+
export interface PdfActionOverride {
|
|
252
|
+
category: PdfCategory;
|
|
253
|
+
detail?: PdfDetail;
|
|
254
|
+
action: PdfFindingAction;
|
|
255
|
+
}
|
|
256
|
+
export interface PdfScoreWeight {
|
|
257
|
+
category: PdfCategory;
|
|
258
|
+
detail?: PdfDetail;
|
|
259
|
+
weight: number;
|
|
260
|
+
}
|
|
261
|
+
/** Lower bound of each band. */
|
|
262
|
+
export interface PdfScoreBands {
|
|
263
|
+
low: number;
|
|
264
|
+
medium: number;
|
|
265
|
+
high: number;
|
|
266
|
+
critical: number;
|
|
267
|
+
}
|
|
268
|
+
export interface PdfOptions {
|
|
269
|
+
/** Default '', tried as the user password and then as the owner password. */
|
|
270
|
+
password?: string;
|
|
271
|
+
/** A limit left out is not enforced. */
|
|
272
|
+
limits?: PdfLimits;
|
|
273
|
+
/** Default false. */
|
|
274
|
+
stripMetadata?: boolean;
|
|
275
|
+
actionOverrides?: PdfActionOverride[];
|
|
276
|
+
scoreWeights?: PdfScoreWeight[];
|
|
277
|
+
scoreBands?: PdfScoreBands;
|
|
278
|
+
filePlugins?: ContainedFilePlugin[];
|
|
279
|
+
scriptPlugins?: ScriptPlugin[];
|
|
280
|
+
/** Directory for temporary files. Defaults to os.tmpdir(). */
|
|
281
|
+
tempDir?: string;
|
|
282
|
+
/**
|
|
283
|
+
* Default 8 MiB. disarmPdfSource keeps its copy of the upload in memory up to this size and in a temporary file
|
|
284
|
+
* above it. Each decoded attachment and object stream above it goes through a temporary file. Scrubbed attachment
|
|
285
|
+
* copies waiting for the write, and an attachment decoded again for its further names, share this much memory. A
|
|
286
|
+
* script that decodes larger than this is removed without going to the script plugins.
|
|
287
|
+
*/
|
|
288
|
+
memoryThreshold?: number;
|
|
289
|
+
/**
|
|
290
|
+
* Default true. A clean file whose signature covers every byte but its own /Contents is copied byte for byte, so
|
|
291
|
+
* the signature stays valid. Every other file that is not rejected is rewritten. False rewrites signed files too.
|
|
292
|
+
*/
|
|
293
|
+
preserveSignatures?: boolean;
|
|
294
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PdfRisk = exports.PdfDetail = exports.PdfCategory = void 0;
|
|
4
|
+
/** High-level kind of a finding. */
|
|
5
|
+
var PdfCategory;
|
|
6
|
+
(function (PdfCategory) {
|
|
7
|
+
PdfCategory["Encrypted"] = "ENCRYPTED";
|
|
8
|
+
PdfCategory["Corrupted"] = "CORRUPTED";
|
|
9
|
+
PdfCategory["JavaScript"] = "JAVASCRIPT";
|
|
10
|
+
PdfCategory["Action"] = "ACTION";
|
|
11
|
+
PdfCategory["Link"] = "LINK";
|
|
12
|
+
PdfCategory["EmbeddedFile"] = "EMBEDDED_FILE";
|
|
13
|
+
PdfCategory["Media"] = "MEDIA";
|
|
14
|
+
PdfCategory["Annotation"] = "ANNOTATION";
|
|
15
|
+
PdfCategory["Form"] = "FORM";
|
|
16
|
+
PdfCategory["Signature"] = "SIGNATURE";
|
|
17
|
+
PdfCategory["Structure"] = "STRUCTURE";
|
|
18
|
+
PdfCategory["Content"] = "CONTENT";
|
|
19
|
+
PdfCategory["Metadata"] = "METADATA";
|
|
20
|
+
PdfCategory["Limit"] = "LIMIT";
|
|
21
|
+
PdfCategory["Processing"] = "PROCESSING";
|
|
22
|
+
})(PdfCategory || (exports.PdfCategory = PdfCategory = {}));
|
|
23
|
+
/** Exact variant of a finding. Some details appear under more than one category. */
|
|
24
|
+
var PdfDetail;
|
|
25
|
+
(function (PdfDetail) {
|
|
26
|
+
// ENCRYPTED
|
|
27
|
+
PdfDetail["EmptyPassword"] = "EMPTY_PASSWORD";
|
|
28
|
+
PdfDetail["UserPassword"] = "USER_PASSWORD";
|
|
29
|
+
PdfDetail["OwnerPassword"] = "OWNER_PASSWORD";
|
|
30
|
+
PdfDetail["PasswordRequired"] = "PASSWORD_REQUIRED";
|
|
31
|
+
PdfDetail["CertificateHandler"] = "CERTIFICATE_HANDLER";
|
|
32
|
+
PdfDetail["UnknownHandler"] = "UNKNOWN_HANDLER";
|
|
33
|
+
PdfDetail["UnknownCryptFilter"] = "UNKNOWN_CRYPT_FILTER";
|
|
34
|
+
PdfDetail["Rc4_40"] = "RC4_40";
|
|
35
|
+
PdfDetail["Rc4_128"] = "RC4_128";
|
|
36
|
+
PdfDetail["Rc4Other"] = "RC4_OTHER";
|
|
37
|
+
PdfDetail["Aes128"] = "AES_128";
|
|
38
|
+
PdfDetail["Aes256"] = "AES_256";
|
|
39
|
+
PdfDetail["MetadataUnencrypted"] = "METADATA_UNENCRYPTED";
|
|
40
|
+
// CORRUPTED
|
|
41
|
+
PdfDetail["Unparseable"] = "UNPARSEABLE";
|
|
42
|
+
PdfDetail["Truncated"] = "TRUNCATED";
|
|
43
|
+
PdfDetail["XrefRebuilt"] = "XREF_REBUILT";
|
|
44
|
+
PdfDetail["MalformedObject"] = "MALFORMED_OBJECT";
|
|
45
|
+
PdfDetail["StreamLengthWrong"] = "STREAM_LENGTH_WRONG";
|
|
46
|
+
PdfDetail["LeadingBytes"] = "LEADING_BYTES";
|
|
47
|
+
PdfDetail["TrailingBytes"] = "TRAILING_BYTES";
|
|
48
|
+
// JAVASCRIPT
|
|
49
|
+
PdfDetail["Document"] = "DOCUMENT";
|
|
50
|
+
PdfDetail["OpenAction"] = "OPEN_ACTION";
|
|
51
|
+
PdfDetail["Page"] = "PAGE";
|
|
52
|
+
PdfDetail["Field"] = "FIELD";
|
|
53
|
+
PdfDetail["Annotation"] = "ANNOTATION";
|
|
54
|
+
PdfDetail["Link"] = "LINK";
|
|
55
|
+
PdfDetail["Bookmark"] = "BOOKMARK";
|
|
56
|
+
PdfDetail["Chained"] = "CHAINED";
|
|
57
|
+
PdfDetail["Rendition"] = "RENDITION";
|
|
58
|
+
PdfDetail["Url"] = "URL";
|
|
59
|
+
PdfDetail["Unattached"] = "UNATTACHED";
|
|
60
|
+
PdfDetail["PluginPassed"] = "PLUGIN_PASSED";
|
|
61
|
+
PdfDetail["PluginScrubbed"] = "PLUGIN_SCRUBBED";
|
|
62
|
+
PdfDetail["PluginFailed"] = "PLUGIN_FAILED";
|
|
63
|
+
PdfDetail["PluginRemoved"] = "PLUGIN_REMOVED";
|
|
64
|
+
// ACTION
|
|
65
|
+
PdfDetail["Launch"] = "LAUNCH";
|
|
66
|
+
PdfDetail["RemoteGoto"] = "REMOTE_GOTO";
|
|
67
|
+
PdfDetail["EmbeddedGoto"] = "EMBEDDED_GOTO";
|
|
68
|
+
PdfDetail["SubmitForm"] = "SUBMIT_FORM";
|
|
69
|
+
PdfDetail["ImportData"] = "IMPORT_DATA";
|
|
70
|
+
PdfDetail["Hide"] = "HIDE";
|
|
71
|
+
PdfDetail["SetLayerState"] = "SET_LAYER_STATE";
|
|
72
|
+
PdfDetail["Named"] = "NAMED";
|
|
73
|
+
PdfDetail["Triggered"] = "TRIGGERED";
|
|
74
|
+
PdfDetail["Unknown"] = "UNKNOWN";
|
|
75
|
+
// LINK
|
|
76
|
+
PdfDetail["Safe"] = "SAFE";
|
|
77
|
+
PdfDetail["FileUrl"] = "FILE_URL";
|
|
78
|
+
PdfDetail["NetworkPath"] = "NETWORK_PATH";
|
|
79
|
+
PdfDetail["DataUrl"] = "DATA_URL";
|
|
80
|
+
PdfDetail["OtherScheme"] = "OTHER_SCHEME";
|
|
81
|
+
PdfDetail["Credentials"] = "CREDENTIALS";
|
|
82
|
+
PdfDetail["IpHost"] = "IP_HOST";
|
|
83
|
+
PdfDetail["LookalikeHost"] = "LOOKALIKE_HOST";
|
|
84
|
+
PdfDetail["EncodedHost"] = "ENCODED_HOST";
|
|
85
|
+
PdfDetail["TextMismatch"] = "TEXT_MISMATCH";
|
|
86
|
+
PdfDetail["FullPage"] = "FULL_PAGE";
|
|
87
|
+
PdfDetail["Relative"] = "RELATIVE";
|
|
88
|
+
// EMBEDDED_FILE
|
|
89
|
+
PdfDetail["NoPlugin"] = "NO_PLUGIN";
|
|
90
|
+
PdfDetail["TypeMismatch"] = "TYPE_MISMATCH";
|
|
91
|
+
PdfDetail["Portfolio"] = "PORTFOLIO";
|
|
92
|
+
// MEDIA
|
|
93
|
+
PdfDetail["Movie"] = "MOVIE";
|
|
94
|
+
PdfDetail["Sound"] = "SOUND";
|
|
95
|
+
PdfDetail["Screen"] = "SCREEN";
|
|
96
|
+
PdfDetail["RichMedia"] = "RICH_MEDIA";
|
|
97
|
+
PdfDetail["ThreeD"] = "THREE_D";
|
|
98
|
+
PdfDetail["Slideshow"] = "SLIDESHOW";
|
|
99
|
+
// ANNOTATION
|
|
100
|
+
PdfDetail["UnknownSubtype"] = "UNKNOWN_SUBTYPE";
|
|
101
|
+
// FORM
|
|
102
|
+
PdfDetail["Fields"] = "FIELDS";
|
|
103
|
+
PdfDetail["Xfa"] = "XFA";
|
|
104
|
+
PdfDetail["CalculationOrder"] = "CALCULATION_ORDER";
|
|
105
|
+
// SIGNATURE
|
|
106
|
+
PdfDetail["Signed"] = "SIGNED";
|
|
107
|
+
PdfDetail["Certified"] = "CERTIFIED";
|
|
108
|
+
PdfDetail["UsageRights"] = "USAGE_RIGHTS";
|
|
109
|
+
// STRUCTURE
|
|
110
|
+
PdfDetail["IncrementalUpdates"] = "INCREMENTAL_UPDATES";
|
|
111
|
+
PdfDetail["UnreferencedObjects"] = "UNREFERENCED_OBJECTS";
|
|
112
|
+
PdfDetail["VersionUpgraded"] = "VERSION_UPGRADED";
|
|
113
|
+
PdfDetail["EscapedNames"] = "ESCAPED_NAMES";
|
|
114
|
+
PdfDetail["ShadowedObjects"] = "SHADOWED_OBJECTS";
|
|
115
|
+
// CONTENT
|
|
116
|
+
PdfDetail["Jbig2Image"] = "JBIG2_IMAGE";
|
|
117
|
+
PdfDetail["JpxImage"] = "JPX_IMAGE";
|
|
118
|
+
PdfDetail["Type3Font"] = "TYPE3_FONT";
|
|
119
|
+
// METADATA
|
|
120
|
+
PdfDetail["InfoDictionary"] = "INFO_DICTIONARY";
|
|
121
|
+
PdfDetail["Xmp"] = "XMP";
|
|
122
|
+
PdfDetail["Stripped"] = "STRIPPED";
|
|
123
|
+
// LIMIT
|
|
124
|
+
PdfDetail["FileSize"] = "FILE_SIZE";
|
|
125
|
+
PdfDetail["ObjectCount"] = "OBJECT_COUNT";
|
|
126
|
+
PdfDetail["DecompressedSize"] = "DECOMPRESSED_SIZE";
|
|
127
|
+
PdfDetail["NestingDepth"] = "NESTING_DEPTH";
|
|
128
|
+
PdfDetail["Time"] = "TIME";
|
|
129
|
+
// PROCESSING
|
|
130
|
+
PdfDetail["MemoryFallback"] = "MEMORY_FALLBACK";
|
|
131
|
+
PdfDetail["VerificationFailed"] = "VERIFICATION_FAILED";
|
|
132
|
+
PdfDetail["ContentRemoved"] = "CONTENT_REMOVED";
|
|
133
|
+
})(PdfDetail || (exports.PdfDetail = PdfDetail = {}));
|
|
134
|
+
var PdfRisk;
|
|
135
|
+
(function (PdfRisk) {
|
|
136
|
+
PdfRisk["None"] = "NONE";
|
|
137
|
+
PdfRisk["Low"] = "LOW";
|
|
138
|
+
PdfRisk["Medium"] = "MEDIUM";
|
|
139
|
+
PdfRisk["High"] = "HIGH";
|
|
140
|
+
PdfRisk["Critical"] = "CRITICAL";
|
|
141
|
+
/** The file could not be inspected: wrong password, unparseable, or a limit stopped the run. */
|
|
142
|
+
PdfRisk["Unknown"] = "UNKNOWN";
|
|
143
|
+
})(PdfRisk || (exports.PdfRisk = PdfRisk = {}));
|
package/dist/uri.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { PdfDetail as D } from './types';
|
|
2
|
+
interface UriVerdict {
|
|
3
|
+
/** LINK detail, or JAVASCRIPT URL for javascript: and vbscript: links. */
|
|
4
|
+
detail: D;
|
|
5
|
+
host?: string;
|
|
6
|
+
/** For a safe target, the hosts a label is compared with, in order. They do not depend on the label. */
|
|
7
|
+
sites?: string[];
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The hosts a label names, as a lookup: has() answers whether a host is one of them or a subdomain of one, or one of
|
|
11
|
+
* them is a subdomain of the host, at a cost that depends on the host and not on how many the label names. No
|
|
12
|
+
* public-suffix list is needed: evil.co.uk is not under mybank.co.uk.
|
|
13
|
+
*/
|
|
14
|
+
export declare class LabelSites {
|
|
15
|
+
private readonly hosts;
|
|
16
|
+
/** Every domain some label host is a subdomain of. */
|
|
17
|
+
private readonly parents;
|
|
18
|
+
constructor(names: string[]);
|
|
19
|
+
get size(): number;
|
|
20
|
+
has(host: string): boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Classifies a link target. `base` is the catalog's URI Base, if any. The walker compares a safe target's `sites`
|
|
24
|
+
* with the hosts a link's labels name.
|
|
25
|
+
*/
|
|
26
|
+
export declare function checkUri(rawUri: string, base?: string): UriVerdict;
|
|
27
|
+
/** Host names written in free text, such as a tooltip. */
|
|
28
|
+
export declare function hostsIn(text: string): string[];
|
|
29
|
+
export {};
|