@patrickdk77/office-defuse 0.1.0 → 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 +278 -36
- package/dist/biff.d.ts +62 -0
- package/dist/biff.js +95 -0
- package/dist/cfb-writer.d.ts +25 -0
- package/dist/cfb-writer.js +314 -0
- package/dist/cfb.d.ts +58 -0
- package/dist/cfb.js +456 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +357 -0
- package/dist/contained.d.ts +46 -0
- package/dist/contained.js +152 -0
- package/dist/context.d.ts +140 -17
- package/dist/context.js +135 -37
- package/dist/crc32.d.ts +8 -2
- package/dist/crc32.js +44 -4
- package/dist/engine.d.ts +12 -1
- package/dist/engine.js +336 -42
- package/dist/errors.d.ts +17 -0
- package/dist/errors.js +27 -1
- package/dist/fields.d.ts +36 -0
- package/dist/fields.js +139 -0
- package/dist/findings.d.ts +2 -1
- package/dist/findings.js +44 -9
- package/dist/flat.d.ts +33 -0
- package/dist/flat.js +486 -0
- package/dist/formats.d.ts +7 -0
- package/dist/formats.js +5 -1
- package/dist/formulas.d.ts +27 -0
- package/dist/formulas.js +157 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +8 -1
- package/dist/index.mjs +1 -1
- package/dist/io.d.ts +62 -1
- package/dist/io.js +245 -1
- package/dist/links.d.ts +27 -0
- package/dist/links.js +106 -1
- package/dist/msg.d.ts +8 -0
- package/dist/msg.js +251 -0
- package/dist/objects.d.ts +45 -0
- package/dist/objects.js +204 -0
- package/dist/odf.d.ts +66 -2
- package/dist/odf.js +181 -64
- package/dist/offcrypto.d.ts +54 -0
- package/dist/offcrypto.js +647 -0
- package/dist/ole.d.ts +12 -0
- package/dist/ole.js +139 -0
- package/dist/oleobj.d.ts +85 -0
- package/dist/oleobj.js +215 -0
- package/dist/ooxml.d.ts +83 -2
- package/dist/ooxml.js +358 -145
- package/dist/package.d.ts +3 -0
- package/dist/package.js +21 -0
- package/dist/plan.d.ts +104 -0
- package/dist/plan.js +73 -0
- package/dist/plugins.d.ts +8 -0
- package/dist/plugins.js +60 -0
- package/dist/ppt.d.ts +8 -0
- package/dist/ppt.js +281 -0
- package/dist/rewrite-cfb.d.ts +14 -0
- package/dist/rewrite-cfb.js +52 -0
- package/dist/rewrite-rtf.d.ts +9 -0
- package/dist/rewrite-rtf.js +45 -0
- package/dist/rewrite-xml.d.ts +36 -0
- package/dist/rewrite-xml.js +341 -0
- package/dist/rewrite-zip.d.ts +8 -0
- package/dist/rewrite-zip.js +498 -0
- package/dist/rtf.d.ts +8 -0
- package/dist/rtf.js +712 -0
- package/dist/rtfcomp.d.ts +7 -0
- package/dist/rtfcomp.js +80 -0
- package/dist/score.js +1 -1
- package/dist/sniff.d.ts +5 -0
- package/dist/sniff.js +38 -0
- package/dist/types.d.ts +66 -2
- package/dist/types.js +33 -0
- package/dist/vsd.d.ts +13 -0
- package/dist/vsd.js +689 -0
- package/dist/word.d.ts +9 -0
- package/dist/word.js +194 -0
- package/dist/xls.d.ts +27 -0
- package/dist/xls.js +208 -0
- package/dist/xml.d.ts +30 -0
- package/dist/xml.js +45 -23
- package/dist/zip-writer.d.ts +4 -0
- package/dist/zip-writer.js +20 -7
- package/dist/zip.d.ts +9 -2
- package/dist/zip.js +61 -4
- package/package.json +6 -3
package/dist/links.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.isAbsoluteTarget = exports.isInsidePackage = exports.isLocalTarget = exports.isFragment = void 0;
|
|
3
|
+
exports.LabelSites = exports.isAbsoluteTarget = exports.isInsidePackage = exports.isLocalTarget = exports.isFragment = void 0;
|
|
4
4
|
exports.checkLink = checkLink;
|
|
5
|
+
exports.labelSite = labelSite;
|
|
6
|
+
exports.mismatchedLabel = mismatchedLabel;
|
|
5
7
|
const types_1 = require("./types");
|
|
6
8
|
const xml_1 = require("./xml");
|
|
7
9
|
const IPV4 = /^\d{1,3}(\.\d{1,3}){3}$/;
|
|
@@ -143,3 +145,106 @@ exports.isInsidePackage = isInsidePackage;
|
|
|
143
145
|
/** Whether an OOXML relationship target is not a part name, whatever TargetMode says: a scheme, a drive or a network path. */
|
|
144
146
|
const isAbsoluteTarget = (rawTarget) => [rawTarget, decoded(rawTarget)].some(t => SCHEME.test(clean(t)) || t.startsWith('\\') || t.startsWith('//'));
|
|
145
147
|
exports.isAbsoluteTarget = isAbsoluteTarget;
|
|
148
|
+
/** A host as LabelSites compares it. */
|
|
149
|
+
const siteName = (h) => h
|
|
150
|
+
.toLowerCase()
|
|
151
|
+
.replace(/^www\./, '')
|
|
152
|
+
.replace(/\.$/, '');
|
|
153
|
+
/**
|
|
154
|
+
* 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
|
|
155
|
+
* them is a subdomain of the host. No public-suffix list is needed: evil.co.uk is not under mybank.co.uk. The same
|
|
156
|
+
* comparison as pdf-defuse's.
|
|
157
|
+
*/
|
|
158
|
+
class LabelSites {
|
|
159
|
+
hosts = new Set();
|
|
160
|
+
/** Every domain some label host is a subdomain of. */
|
|
161
|
+
parents = new Set();
|
|
162
|
+
constructor(names) {
|
|
163
|
+
for (const n of names) {
|
|
164
|
+
const x = siteName(n);
|
|
165
|
+
this.hosts.add(x);
|
|
166
|
+
for (let i = x.indexOf('.'); i >= 0; i = x.indexOf('.', i + 1))
|
|
167
|
+
this.parents.add(x.slice(i + 1));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
get size() {
|
|
171
|
+
return this.hosts.size;
|
|
172
|
+
}
|
|
173
|
+
has(host) {
|
|
174
|
+
const y = siteName(host);
|
|
175
|
+
if (this.hosts.has(y) || this.parents.has(y))
|
|
176
|
+
return true;
|
|
177
|
+
for (let i = y.indexOf('.'); i >= 0; i = y.indexOf('.', i + 1))
|
|
178
|
+
if (this.hosts.has(y.slice(i + 1)))
|
|
179
|
+
return true;
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
exports.LabelSites = LabelSites;
|
|
184
|
+
/**
|
|
185
|
+
* Top-level domains a bare host in a label may end in: the common generic ones and the country codes. A name that
|
|
186
|
+
* ends in anything else, such as an Excel function (BETA.DIST) or a file (moon.htm), is not read as an address.
|
|
187
|
+
*/
|
|
188
|
+
const TLDS = new Set(('com net org edu gov mil int info biz name pro mobi asia tel travel jobs museum aero coop app dev io ai co me tv cc ly ' +
|
|
189
|
+
'xyz online site top shop club store tech live page blog cloud email link news ' +
|
|
190
|
+
'ac ad ae af ag ai al am ao aq ar as at au aw ax az ba bb bd be bf bg bh bi bj bm bn bo br bs bt bw by bz ca cd cf cg ' +
|
|
191
|
+
'ch ci ck cl cm cn cr cu cv cw cx cy cz de dj dk dm do dz ec ee eg er es et eu fi fj fk fm fo fr ga gd ge gf gg gh gi ' +
|
|
192
|
+
'gl gm gn gp gq gr gs gt gu gw gy hk hm hn hr ht hu id ie il im in iq ir is it je jm jo jp ke kg kh ki km kn kp kr kw ' +
|
|
193
|
+
'ky kz la lb lc li lk lr ls lt lu lv ma mc md mg mh mk ml mm mn mo mp mq mr ms mt mu mv mw mx my mz na nc ne nf ng ni ' +
|
|
194
|
+
'nl no np nr nu nz om pa pe pf pg ph pk pl pm pn pr ps pt pw py qa re ro rs ru rw sa sb sc sd se sg sh si sk sl sm sn ' +
|
|
195
|
+
'so sr ss st su sv sx sy sz tc td tf tg th tj tk tl tm tn to tr tt tw tz ua ug uk us uy uz va vc ve vg vi vn vu wf ws ' +
|
|
196
|
+
'ye yt za zm zw').split(' '));
|
|
197
|
+
/** The most of a label that is read. */
|
|
198
|
+
const MAX_LABEL = 4096;
|
|
199
|
+
/**
|
|
200
|
+
* The site a label names when the label is itself an address: a web URL, a mail address, or a host with or without a
|
|
201
|
+
* path that starts with www. or ends in a known top-level domain. Text that only mentions an address, an Excel
|
|
202
|
+
* function name and a file name name no site. Excel's function names are all capitals, as typed hosts never are, so
|
|
203
|
+
* a bare host in capitals counts only after WWW.
|
|
204
|
+
*/
|
|
205
|
+
function labelSite(raw) {
|
|
206
|
+
const t = raw
|
|
207
|
+
.slice(0, MAX_LABEL)
|
|
208
|
+
.replace(/\p{Cc}/gu, '')
|
|
209
|
+
.trim();
|
|
210
|
+
if (/^https?:\/\//i.test(t)) {
|
|
211
|
+
try {
|
|
212
|
+
return new URL(t).hostname.toLowerCase() || undefined;
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const mail = /^(?:mailto:)?[^\s@/]+@([a-z0-9-]+(?:\.[a-z0-9-]+)+)$/i.exec(t);
|
|
219
|
+
const host = mail?.[1] ?? /^([a-z0-9-]+(?:\.[a-z0-9-]+)+)(?::\d+)?(?:[/?#]\S*)?$/i.exec(t)?.[1];
|
|
220
|
+
if (!host)
|
|
221
|
+
return undefined;
|
|
222
|
+
const h = host.toLowerCase();
|
|
223
|
+
if (h.startsWith('www.'))
|
|
224
|
+
return h;
|
|
225
|
+
if (!mail && host === host.toUpperCase() && /[A-Z]/.test(host))
|
|
226
|
+
return undefined;
|
|
227
|
+
return TLDS.has(h.slice(h.lastIndexOf('.') + 1)) ? h : undefined;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* The first label of a safe link that is an address, and an address of another site than the one the link opens, or
|
|
231
|
+
* undefined. A label is text the reader sees for the link: its text, its tooltip, a display name. A subdomain or a
|
|
232
|
+
* parent of the label's site counts as the same site, as in pdf-defuse. A target that fails on its own gets its own
|
|
233
|
+
* finding instead.
|
|
234
|
+
*/
|
|
235
|
+
function mismatchedLabel(target, labels) {
|
|
236
|
+
if (!labels.some(Boolean))
|
|
237
|
+
return undefined;
|
|
238
|
+
const v = checkLink(target);
|
|
239
|
+
if (v.category !== types_1.OfficeCategory.Link || v.detail !== types_1.OfficeDetail.Safe || !v.host)
|
|
240
|
+
return undefined;
|
|
241
|
+
for (const label of labels) {
|
|
242
|
+
const site = label ? labelSite(label) : undefined;
|
|
243
|
+
// A site without a dot, where the target has one, is a label cut short or a local alias, and names no site.
|
|
244
|
+
if (!site || (!site.includes('.') && v.host.includes('.')))
|
|
245
|
+
continue;
|
|
246
|
+
if (!new LabelSites([site]).has(v.host))
|
|
247
|
+
return label;
|
|
248
|
+
}
|
|
249
|
+
return undefined;
|
|
250
|
+
}
|
package/dist/msg.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type CfbEntry, type CompoundFile } from './cfb';
|
|
2
|
+
import { RunContext } from './context';
|
|
3
|
+
import type { CompoundPlan } from './plan';
|
|
4
|
+
/**
|
|
5
|
+
* Inspects an Outlook message (MS-OXMSG): each attachment is reported with its name and type, and an attachment by
|
|
6
|
+
* reference with what it points to. An embedded message's own attachments are read as well.
|
|
7
|
+
*/
|
|
8
|
+
export declare function messageRules(ctx: RunContext, cfb: CompoundFile, message: CfbEntry, depth?: number, plan?: CompoundPlan): Promise<void>;
|
package/dist/msg.js
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.messageRules = messageRules;
|
|
4
|
+
const cfb_1 = require("./cfb");
|
|
5
|
+
const contained_1 = require("./contained");
|
|
6
|
+
const context_1 = require("./context");
|
|
7
|
+
const errors_1 = require("./errors");
|
|
8
|
+
const io_1 = require("./io");
|
|
9
|
+
const oleobj_1 = require("./oleobj");
|
|
10
|
+
const rewrite_rtf_1 = require("./rewrite-rtf");
|
|
11
|
+
const rtf_1 = require("./rtf");
|
|
12
|
+
const rtfcomp_1 = require("./rtfcomp");
|
|
13
|
+
const types_1 = require("./types");
|
|
14
|
+
/** Property ids of an attachment (MS-OXPROPS). */
|
|
15
|
+
const P = { method: 0x3705, longName: 0x3707, name: 0x3704, display: 0x3001, mime: 0x370e, data: 0x3701, longPath: 0x370d, path: 0x3708 };
|
|
16
|
+
/** How an attachment holds its file (MS-OXCMSG 2.2.2.9): by value, by reference, embedded message, OLE object, web reference. */
|
|
17
|
+
const METHODS = { 1: 'by value', 2: 'by reference', 3: 'by reference, resolved', 4: 'by reference only', 5: 'embedded message', 6: 'OLE object', 7: 'by web reference' };
|
|
18
|
+
/** Embedded messages can hold messages in turn. Past this depth they are still reported, and not read further. */
|
|
19
|
+
const MAX_DEPTH = 16;
|
|
20
|
+
/** A message's body properties (MS-OXPROPS): PidTagRtfCompressed, and PidTagHtml as binary, Unicode or 8-bit text. */
|
|
21
|
+
const RTF_BODY = 0x10090102;
|
|
22
|
+
const HTML_BODIES = [0x10130102, 0x1013001f, 0x1013001e];
|
|
23
|
+
/** The stream that holds a variable-size property (MS-OXMSG 2.1.4.1). */
|
|
24
|
+
const streamName = (tag) => `__SUBSTG1.0_${tag.toString(16).toUpperCase().padStart(8, '0')}`;
|
|
25
|
+
/**
|
|
26
|
+
* Inspects an Outlook message (MS-OXMSG): each attachment is reported with its name and type, and an attachment by
|
|
27
|
+
* reference with what it points to. An embedded message's own attachments are read as well.
|
|
28
|
+
*/
|
|
29
|
+
async function messageRules(ctx, cfb, message, depth = 0, plan) {
|
|
30
|
+
const edit = { attachmentsDropped: 0, drop: new Set(), sizes: new Map() };
|
|
31
|
+
await bodyRules(ctx, cfb, message, edit, plan);
|
|
32
|
+
for (const a of message.children) {
|
|
33
|
+
if (a.kind !== 'storage' || !/^__attach_version1\.0_#[0-9A-F]{8}$/i.test(a.name))
|
|
34
|
+
continue;
|
|
35
|
+
// An attachment a strip finding names goes with its whole storage.
|
|
36
|
+
if ((await attachmentRules(ctx, cfb, a, depth, plan)).some(r => r.action === 'strip') && plan) {
|
|
37
|
+
plan.drop.add(a);
|
|
38
|
+
edit.attachmentsDropped++;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (plan && (edit.attachmentsDropped || edit.drop.size || edit.sizes.size))
|
|
42
|
+
await editProperties(ctx, cfb, message, edit, plan);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The property stream of a message after the rewrite (MS-OXMSG 2.4). Its header counts the attachments, at the same
|
|
46
|
+
* place in a message and in an embedded one, and the count drops by the attachments that went. Each property held in
|
|
47
|
+
* a stream has an entry giving that stream's size: the entry goes with a body that went, and takes the new size of a
|
|
48
|
+
* body that was rewritten.
|
|
49
|
+
*/
|
|
50
|
+
async function editProperties(ctx, cfb, message, edit, plan) {
|
|
51
|
+
const s = message.children.find(c => c.kind === 'stream' && c.name.toUpperCase() === '__PROPERTIES_VERSION1.0');
|
|
52
|
+
if (!s || s.size < 24 || s.size > ctx.settings.decompressedBytes)
|
|
53
|
+
return;
|
|
54
|
+
const b = Buffer.from(await cfb.bytes(s));
|
|
55
|
+
const headerSize = message === cfb.root ? 32 : 24;
|
|
56
|
+
if (edit.attachmentsDropped)
|
|
57
|
+
b.writeUInt32LE(Math.max(0, b.readUInt32LE(20) - edit.attachmentsDropped), 20);
|
|
58
|
+
const kept = [b.subarray(0, headerSize)];
|
|
59
|
+
for (let at = headerSize; at + 16 <= b.length; at += 16) {
|
|
60
|
+
const entry = Buffer.from(b.subarray(at, at + 16));
|
|
61
|
+
const tag = entry.readUInt32LE(0);
|
|
62
|
+
if (edit.drop.has(tag))
|
|
63
|
+
continue;
|
|
64
|
+
const size = edit.sizes.get(tag);
|
|
65
|
+
if (size !== undefined)
|
|
66
|
+
entry.writeUInt32LE(size, 8);
|
|
67
|
+
kept.push(entry);
|
|
68
|
+
}
|
|
69
|
+
plan.replace.set(s, await (0, contained_1.spillOf)(ctx.run, Buffer.concat(kept)));
|
|
70
|
+
}
|
|
71
|
+
/** A stream of the message storage by property tag. */
|
|
72
|
+
const bodyStream = (message, tag) => message.children.find(c => c.kind === 'stream' && c.name.toUpperCase() === streamName(tag));
|
|
73
|
+
/** Every byte a sink holds. */
|
|
74
|
+
async function bytesOf(sink) {
|
|
75
|
+
const chunks = [];
|
|
76
|
+
for await (const c of sink.read())
|
|
77
|
+
chunks.push(Buffer.from(c));
|
|
78
|
+
return Buffer.concat(chunks);
|
|
79
|
+
}
|
|
80
|
+
async function* once(b) {
|
|
81
|
+
yield b;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* A message's bodies. The RTF body goes to the plugins as the RTF it decompresses to, and a plugin's copy is stored
|
|
85
|
+
* back uncompressed. When no plugin takes it, the RTF rules read it: a strip finding inside rewrites it, and a finding
|
|
86
|
+
* that rejects it, or a body that does not decompress, makes it go, leaving the plain text body. The HTML body goes
|
|
87
|
+
* to the plugins, and when none takes it, it stays, as no rule names it.
|
|
88
|
+
*/
|
|
89
|
+
async function bodyRules(ctx, cfb, message, edit, plan) {
|
|
90
|
+
const replace = async (stream, tag, bytes) => {
|
|
91
|
+
plan?.replace.set(stream, await (0, contained_1.spillOf)(ctx.run, bytes));
|
|
92
|
+
edit.sizes.set(tag, bytes.length);
|
|
93
|
+
};
|
|
94
|
+
const drop = (stream, tag) => {
|
|
95
|
+
plan?.drop.add(stream);
|
|
96
|
+
edit.drop.add(tag);
|
|
97
|
+
};
|
|
98
|
+
const rtf = bodyStream(message, RTF_BODY);
|
|
99
|
+
if (rtf) {
|
|
100
|
+
const location = (0, cfb_1.entryPath)(rtf);
|
|
101
|
+
const body = rtf.size <= ctx.settings.decompressedBytes ? (0, rtfcomp_1.decompressRtf)(await cfb.bytes(rtf), ctx.settings.decompressedBytes) : undefined;
|
|
102
|
+
if (!body) {
|
|
103
|
+
if (ctx.report(types_1.OfficeCategory.Package, types_1.OfficeDetail.UnreadableBody, location, { stream: location }).action === 'strip')
|
|
104
|
+
drop(rtf, RTF_BODY);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
const decision = await (0, contained_1.decideContained)(ctx, { name: 'RTF body', declaredType: 'application/rtf', location, chunks: () => once(body) });
|
|
108
|
+
if (decision) {
|
|
109
|
+
if (!decision.keep)
|
|
110
|
+
drop(rtf, RTF_BODY);
|
|
111
|
+
else if (decision.replacement)
|
|
112
|
+
await replace(rtf, RTF_BODY, (0, rtfcomp_1.storeRtf)(await bytesOf(decision.replacement)));
|
|
113
|
+
}
|
|
114
|
+
else
|
|
115
|
+
await rtfRules(ctx, body, location, rtf, edit, plan);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
for (const tag of HTML_BODIES) {
|
|
119
|
+
const html = bodyStream(message, tag);
|
|
120
|
+
if (!html || html.size > ctx.settings.decompressedBytes)
|
|
121
|
+
continue;
|
|
122
|
+
const raw = await cfb.bytes(html);
|
|
123
|
+
const unicode = (tag & 0xffff) === 0x001f;
|
|
124
|
+
const text = unicode ? Buffer.from((0, oleobj_1.utf16)(raw).replace(/\0+$/, ''), 'utf8') : raw;
|
|
125
|
+
const decision = await (0, contained_1.decideContained)(ctx, { name: 'HTML body', declaredType: 'text/html', location: (0, cfb_1.entryPath)(html), chunks: () => once(text) });
|
|
126
|
+
if (decision && !decision.keep)
|
|
127
|
+
drop(html, tag);
|
|
128
|
+
else if (decision?.replacement) {
|
|
129
|
+
const out = await bytesOf(decision.replacement);
|
|
130
|
+
await replace(html, tag, unicode ? Buffer.from(`${out.toString('utf8')}\0`, 'utf16le') : out);
|
|
131
|
+
}
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* The RTF rules over a message's RTF body, reporting into this run with the body's place. A strip finding rewrites the
|
|
137
|
+
* body, and a reject drops it.
|
|
138
|
+
*/
|
|
139
|
+
async function rtfRules(ctx, body, location, stream, edit, plan) {
|
|
140
|
+
const sub = new context_1.RunContext(ctx.factory, ctx.xmlLimits, ctx.settings, ctx.run);
|
|
141
|
+
// The RTF reader leaves its cuts where a whole file's rewrite finds them, which is the message's own.
|
|
142
|
+
const saved = ctx.run.output;
|
|
143
|
+
let cuts;
|
|
144
|
+
try {
|
|
145
|
+
await (0, rtf_1.inspectRtf)(sub, (0, io_1.bufferSource)(body), body.length);
|
|
146
|
+
const out = ctx.run.output;
|
|
147
|
+
if (out?.kind === 'rtf')
|
|
148
|
+
cuts = out;
|
|
149
|
+
}
|
|
150
|
+
catch (e) {
|
|
151
|
+
if ((0, errors_1.stopsTree)(e) || !(e instanceof errors_1.StopError))
|
|
152
|
+
throw e;
|
|
153
|
+
sub.report(e.category, e.detail, undefined, { ...e.data, reason: e.data?.reason ?? e.message });
|
|
154
|
+
cuts = undefined;
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
ctx.run.output = saved;
|
|
158
|
+
}
|
|
159
|
+
const found = sub.findings;
|
|
160
|
+
(0, contained_1.pushContained)(ctx, found, location, 'RTF body');
|
|
161
|
+
if (found.some(f => f.action === 'reject') || !cuts) {
|
|
162
|
+
if (found.some(f => f.action !== 'info')) {
|
|
163
|
+
plan?.drop.add(stream);
|
|
164
|
+
edit.drop.add(RTF_BODY);
|
|
165
|
+
}
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (!found.some(f => f.action === 'strip') || !plan)
|
|
169
|
+
return;
|
|
170
|
+
const sink = new io_1.SpillSink(ctx.run.temp, ctx.run.threshold);
|
|
171
|
+
ctx.run.held.push(sink);
|
|
172
|
+
await (0, rewrite_rtf_1.writeRtf)(ctx.run, (0, io_1.bufferSource)(body), body.length, cuts.cuts, sink);
|
|
173
|
+
await sink.close();
|
|
174
|
+
const stored = (0, rtfcomp_1.storeRtf)(await bytesOf(sink));
|
|
175
|
+
plan.replace.set(stream, await (0, contained_1.spillOf)(ctx.run, stored));
|
|
176
|
+
edit.sizes.set(RTF_BODY, stored.length);
|
|
177
|
+
}
|
|
178
|
+
async function attachmentRules(ctx, cfb, a, depth, plan) {
|
|
179
|
+
const location = (0, cfb_1.entryPath)(a);
|
|
180
|
+
const props = await properties(cfb, a);
|
|
181
|
+
const text = async (id) => {
|
|
182
|
+
for (const [type, decode] of [
|
|
183
|
+
['001F', oleobj_1.utf16],
|
|
184
|
+
['001E', oleobj_1.latin1],
|
|
185
|
+
]) {
|
|
186
|
+
const s = a.children.find(c => c.kind === 'stream' && c.name.toUpperCase() === `__SUBSTG1.0_${id.toString(16).toUpperCase().padStart(4, '0')}${type}`);
|
|
187
|
+
if (s && s.size <= 65536)
|
|
188
|
+
return decode(await cfb.bytes(s)).replace(/\0+$/, '');
|
|
189
|
+
}
|
|
190
|
+
return '';
|
|
191
|
+
};
|
|
192
|
+
const method = props.get(P.method) ?? 0;
|
|
193
|
+
const name = (await text(P.longName)) || (await text(P.name)) || (await text(P.display));
|
|
194
|
+
const declaredType = await text(P.mime);
|
|
195
|
+
const dataId = `__SUBSTG1.0_${P.data.toString(16).toUpperCase()}`;
|
|
196
|
+
const data = a.children.find(c => c.name.toUpperCase().startsWith(dataId));
|
|
197
|
+
const info = { attachment: location, method: METHODS[method] ?? String(method) };
|
|
198
|
+
if (name)
|
|
199
|
+
info.name = (0, context_1.clip)(name);
|
|
200
|
+
if (declaredType)
|
|
201
|
+
info.type = (0, context_1.clip)(declaredType);
|
|
202
|
+
if (method >= 2 && method <= 4) {
|
|
203
|
+
const path = (await text(P.longPath)) || (await text(P.path));
|
|
204
|
+
return path ? [ctx.external(types_1.OfficeDetail.Other, path, location, { source: 'attachment by reference', ...(name ? { name: (0, context_1.clip)(name) } : {}) }, 'attachment')] : [];
|
|
205
|
+
}
|
|
206
|
+
if (method === 7) {
|
|
207
|
+
const path = (await text(P.longPath)) || (await text(P.path));
|
|
208
|
+
return path ? [ctx.external(types_1.OfficeDetail.Other, path, location, { source: 'attachment by web reference', ...(name ? { name: (0, context_1.clip)(name) } : {}) }, 'attachment')] : [];
|
|
209
|
+
}
|
|
210
|
+
if (!data)
|
|
211
|
+
return [];
|
|
212
|
+
if (data.kind === 'stream') {
|
|
213
|
+
return [await attachment(ctx, cfb, data, { name: name || undefined, declaredType: declaredType || undefined, location }, info, plan)];
|
|
214
|
+
}
|
|
215
|
+
// An embedded message or an OLE object is a storage of this file. The object rules read an object's storage, and
|
|
216
|
+
// an embedded message's attachments are read here.
|
|
217
|
+
info.size = 0;
|
|
218
|
+
if (method === 5)
|
|
219
|
+
info.type ??= 'application/vnd.ms-outlook';
|
|
220
|
+
const r = ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Attachment, location, info);
|
|
221
|
+
if (method === 5 && depth < MAX_DEPTH)
|
|
222
|
+
await messageRules(ctx, cfb, data, depth + 1, plan);
|
|
223
|
+
return [r];
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* An attached file held in a stream. It goes to the plugins, whose verdict decides whether it stays. When no plugin
|
|
227
|
+
* takes it, EMBEDDED/ATTACHMENT does.
|
|
228
|
+
*/
|
|
229
|
+
async function attachment(ctx, cfb, data, file, info, plan) {
|
|
230
|
+
const source = cfb.source(data);
|
|
231
|
+
const decision = await (0, contained_1.decideContained)(ctx, { ...file, chunks: () => (0, io_1.chunksOf)(source, data.size) });
|
|
232
|
+
if (decision?.replacement)
|
|
233
|
+
plan?.replace.set(data, decision.replacement);
|
|
234
|
+
const r = ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Attachment, file.location, { ...info, size: data.size }, (0, contained_1.presence)(decision));
|
|
235
|
+
return decision && !decision.keep ? { ...r, action: 'strip' } : r;
|
|
236
|
+
}
|
|
237
|
+
/** The fixed-size values of a property stream (MS-OXMSG 2.4.2), by property id. An attachment's has an 8-byte header. */
|
|
238
|
+
async function properties(cfb, storage) {
|
|
239
|
+
const out = new Map();
|
|
240
|
+
const s = storage.children.find(c => c.kind === 'stream' && c.name.toUpperCase() === '__PROPERTIES_VERSION1.0');
|
|
241
|
+
if (!s || s.size > 65536)
|
|
242
|
+
return out;
|
|
243
|
+
const b = await cfb.bytes(s);
|
|
244
|
+
for (let at = 8; at + 16 <= b.length; at += 16) {
|
|
245
|
+
const tag = (b[at] | (b[at + 1] << 8) | (b[at + 2] << 16) | (b[at + 3] << 24)) >>> 0;
|
|
246
|
+
// PT_LONG values sit in the first four bytes of the value.
|
|
247
|
+
if ((tag & 0xffff) === 0x0003)
|
|
248
|
+
out.set(tag >>> 16, (b[at + 8] | (b[at + 9] << 8) | (b[at + 10] << 16) | (b[at + 11] << 24)) >>> 0);
|
|
249
|
+
}
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type CfbEntry, CompoundFile } from './cfb';
|
|
2
|
+
import { type ContainedDecision } from './contained';
|
|
3
|
+
import { type Reported, type RunContext } from './context';
|
|
4
|
+
import { type PackagedFile } from './oleobj';
|
|
5
|
+
import type { CompoundPlan } from './plan';
|
|
6
|
+
/** Equation Editor 3.0's ProgID, which rtfobj matches as a prefix. */
|
|
7
|
+
export declare const isEquationClass: (name: string) => boolean;
|
|
8
|
+
/** Reports what the reader of a compound file inside this one noted, at the place it sits. */
|
|
9
|
+
export declare function nestedNotes(ctx: RunContext, cfb: CompoundFile, location: string): void;
|
|
10
|
+
/**
|
|
11
|
+
* The rules every compound file gets: VBA projects, embedded objects and controls, wherever they sit in the tree.
|
|
12
|
+
* `prefix` names where the file sits when it is inside another, and `rootIsObject` says that its root is itself an
|
|
13
|
+
* OLE object's storage, as an object in a presentation or an RTF file is. `plan`, given for the file the rewrite
|
|
14
|
+
* writes, collects the storages a strip finding names.
|
|
15
|
+
*/
|
|
16
|
+
export declare function compoundRules(ctx: RunContext, cfb: CompoundFile, prefix: string, rootIsObject: boolean, plan?: CompoundPlan, rootDecision?: ContainedDecision): Promise<void>;
|
|
17
|
+
/** What an OLE object's storage says about the object. */
|
|
18
|
+
export interface ObjectInfo {
|
|
19
|
+
clsid: string;
|
|
20
|
+
progId?: string;
|
|
21
|
+
userType?: string;
|
|
22
|
+
/** An ActiveX control's storage, with the control's name. */
|
|
23
|
+
control: boolean;
|
|
24
|
+
controlName?: string;
|
|
25
|
+
linked: boolean;
|
|
26
|
+
/** What a linked object loads, or ''. */
|
|
27
|
+
target: string;
|
|
28
|
+
/** The target is a URL moniker's, which loads from outside however it is spelled. */
|
|
29
|
+
url: boolean;
|
|
30
|
+
/** A Packager object's file, held or linked to. */
|
|
31
|
+
pkg?: PackagedFile;
|
|
32
|
+
equation: boolean;
|
|
33
|
+
}
|
|
34
|
+
/** Reads an OLE object's storage: its class, its CompObj and OLE streams, and Packager's native data. */
|
|
35
|
+
export declare function describeObject(cfb: CompoundFile, e: CfbEntry, _location: string): Promise<ObjectInfo>;
|
|
36
|
+
/**
|
|
37
|
+
* Reports a linked object. A URL moniker's target loads from outside the file whatever its text looks like: a
|
|
38
|
+
* CVE-2017-0199 sample puts a character with a stray high byte where the colon of https: goes.
|
|
39
|
+
*/
|
|
40
|
+
export declare function linkedObject(ctx: RunContext, d: {
|
|
41
|
+
target: string;
|
|
42
|
+
url: boolean;
|
|
43
|
+
}, location: string, data: Record<string, string>): Reported;
|
|
44
|
+
/** What a finding shows of a Packager object's file: its name and paths, and its size when the object holds it. */
|
|
45
|
+
export declare function packageData(p: PackagedFile): Record<string, string | number>;
|
package/dist/objects.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isEquationClass = void 0;
|
|
4
|
+
exports.nestedNotes = nestedNotes;
|
|
5
|
+
exports.compoundRules = compoundRules;
|
|
6
|
+
exports.describeObject = describeObject;
|
|
7
|
+
exports.linkedObject = linkedObject;
|
|
8
|
+
exports.packageData = packageData;
|
|
9
|
+
const cfb_1 = require("./cfb");
|
|
10
|
+
const cfb_writer_1 = require("./cfb-writer");
|
|
11
|
+
const contained_1 = require("./contained");
|
|
12
|
+
const context_1 = require("./context");
|
|
13
|
+
const errors_1 = require("./errors");
|
|
14
|
+
const io_1 = require("./io");
|
|
15
|
+
const oleobj_1 = require("./oleobj");
|
|
16
|
+
const rewrite_cfb_1 = require("./rewrite-cfb");
|
|
17
|
+
const types_1 = require("./types");
|
|
18
|
+
/** The class ids of Equation Editor 3.0 and its kin, which CVE-2017-11882 and CVE-2018-0802 exploit, as oletools lists them. */
|
|
19
|
+
const EQUATION_CLSIDS = new Set(['0002CE02-0000-0000-C000-000000000046', '00021700-0000-0000-C000-000000000046', '0003000B-0000-0000-C000-000000000046', '0004A6B0-0000-0000-C000-000000000046']);
|
|
20
|
+
/** Equation Editor 3.0's ProgID, which rtfobj matches as a prefix. */
|
|
21
|
+
const isEquationClass = (name) => name.trim().toLowerCase().startsWith('equation.3');
|
|
22
|
+
exports.isEquationClass = isEquationClass;
|
|
23
|
+
/** Streams that only an OLE object's storage holds. */
|
|
24
|
+
const OBJECT_STREAMS = new Set(['\x01OLE', '\x01COMPOBJ', '\x01OLE10NATIVE', '\x03OBJINFO']);
|
|
25
|
+
/** Streams that only an ActiveX control's storage holds. */
|
|
26
|
+
const CONTROL_STREAMS = new Set(['\x03OCXNAME', '\x03OCXDATA']);
|
|
27
|
+
/** A CompObj, OLE or ObjInfo stream is small. More is not read. */
|
|
28
|
+
const SMALL = 65536;
|
|
29
|
+
/** Packager's header is short, and its names are at most 1,024 bytes each. */
|
|
30
|
+
const PACKAGE_HEAD = 4096;
|
|
31
|
+
const upper = (e) => e.name.toUpperCase();
|
|
32
|
+
/** Reports what the reader of a compound file inside this one noted, at the place it sits. */
|
|
33
|
+
function nestedNotes(ctx, cfb, location) {
|
|
34
|
+
for (const n of cfb.notes)
|
|
35
|
+
ctx.report(types_1.OfficeCategory.Container, n.detail, location, { bytes: n.bytes });
|
|
36
|
+
}
|
|
37
|
+
/** Where a finding about an entry points: `prefix`, then the entry's path. */
|
|
38
|
+
const at = (prefix, e) => [prefix, (0, cfb_1.entryPath)(e)].filter(Boolean).join(' > ');
|
|
39
|
+
/**
|
|
40
|
+
* The rules every compound file gets: VBA projects, embedded objects and controls, wherever they sit in the tree.
|
|
41
|
+
* `prefix` names where the file sits when it is inside another, and `rootIsObject` says that its root is itself an
|
|
42
|
+
* OLE object's storage, as an object in a presentation or an RTF file is. `plan`, given for the file the rewrite
|
|
43
|
+
* writes, collects the storages a strip finding names.
|
|
44
|
+
*/
|
|
45
|
+
async function compoundRules(ctx, cfb, prefix, rootIsObject, plan, rootDecision) {
|
|
46
|
+
const projects = new Set();
|
|
47
|
+
const drop = (r, e) => {
|
|
48
|
+
if (r.action === 'strip' && e.kind !== 'root')
|
|
49
|
+
plan?.drop.add(e);
|
|
50
|
+
};
|
|
51
|
+
for (const e of cfb.entries) {
|
|
52
|
+
if (e.kind === 'stream')
|
|
53
|
+
continue;
|
|
54
|
+
const child = (name) => e.children.find(c => upper(c) === name);
|
|
55
|
+
// A VBA project: a VBA storage with a dir stream, which names the modules (MS-OVBA 2.2). The storage that holds
|
|
56
|
+
// the project goes, or the VBA storage when that is the root.
|
|
57
|
+
const vba = child('VBA');
|
|
58
|
+
if (vba?.kind === 'storage' && vba.children.some(c => c.kind === 'stream' && upper(c) === 'DIR')) {
|
|
59
|
+
projects.add(e);
|
|
60
|
+
drop(ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.VbaProject, at(prefix, vba), { storage: at(prefix, vba) }), e.kind === 'root' ? vba : e);
|
|
61
|
+
}
|
|
62
|
+
if (e.kind === 'storage' && (upper(e) === '_VBA_PROJECT_CUR' || upper(e) === 'MACROS') && e.children.length && !projects.has(e)) {
|
|
63
|
+
// Excel's and Word's project storages, without the dir stream a whole project has.
|
|
64
|
+
drop(ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.VbaProject, at(prefix, e), { storage: at(prefix, e) }), e);
|
|
65
|
+
}
|
|
66
|
+
const names = new Set(e.children.map(upper));
|
|
67
|
+
const parent = e.parent ? upper(e.parent) : '';
|
|
68
|
+
const isObject = e.kind === 'root'
|
|
69
|
+
? rootIsObject
|
|
70
|
+
: [...names].some(n => OBJECT_STREAMS.has(n) || CONTROL_STREAMS.has(n)) ||
|
|
71
|
+
(parent === 'OBJECTPOOL' && e.parent?.parent?.kind === 'root') ||
|
|
72
|
+
(e.parent?.kind === 'root' && /^MBD[0-9A-F]{8}$/i.test(e.name));
|
|
73
|
+
if (isObject)
|
|
74
|
+
for (const r of await objectRules(ctx, cfb, e, at(prefix, e), plan, e.kind === 'root' ? rootDecision : undefined))
|
|
75
|
+
drop(r, e);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** Reads an OLE object's storage: its class, its CompObj and OLE streams, and Packager's native data. */
|
|
79
|
+
async function describeObject(cfb, e, _location) {
|
|
80
|
+
const child = (name) => e.children.find(c => c.kind === 'stream' && upper(c) === name);
|
|
81
|
+
const small = async (name) => {
|
|
82
|
+
const s = child(name);
|
|
83
|
+
return s && s.size <= SMALL ? cfb.bytes(s) : undefined;
|
|
84
|
+
};
|
|
85
|
+
const clsid = e.clsid === '00000000-0000-0000-0000-000000000000' ? '' : e.clsid;
|
|
86
|
+
const compObjBytes = await small('\x01COMPOBJ');
|
|
87
|
+
const comp = compObjBytes ? (0, oleobj_1.parseCompObj)(compObjBytes) : {};
|
|
88
|
+
const info = { clsid, progId: comp.progId, userType: comp.userType, control: false, linked: false, target: '', url: false, equation: false };
|
|
89
|
+
info.equation = EQUATION_CLSIDS.has(clsid) || (0, exports.isEquationClass)(comp.progId ?? '');
|
|
90
|
+
if ([...CONTROL_STREAMS].some(n => child(n))) {
|
|
91
|
+
const name = await small('\x03OCXNAME');
|
|
92
|
+
info.control = true;
|
|
93
|
+
if (name)
|
|
94
|
+
info.controlName = Buffer.from(name).toString('utf16le').replace(/\0+$/, '');
|
|
95
|
+
return info;
|
|
96
|
+
}
|
|
97
|
+
const oleBytes = await small('\x01OLE');
|
|
98
|
+
const ole = oleBytes ? (0, oleobj_1.parseOleStream)(oleBytes) : { linked: false, target: '', url: false };
|
|
99
|
+
Object.assign(info, ole);
|
|
100
|
+
const native = child('\x01OLE10NATIVE');
|
|
101
|
+
if (native) {
|
|
102
|
+
// The stream starts with the native data's size, then Packager's data.
|
|
103
|
+
info.pkg = (0, oleobj_1.parsePackage)(await cfb.source(native).read(4, PACKAGE_HEAD));
|
|
104
|
+
}
|
|
105
|
+
return info;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Shows an embedded object of the compound file the rewrite writes to the plugins: the file a Packager object holds,
|
|
109
|
+
* or the object's storage written out as a compound file of its own. Bytes a plugin wrote take the file's place in
|
|
110
|
+
* the Ole10Native stream, or the storage's place as the storages and streams of the compound file they hold.
|
|
111
|
+
*/
|
|
112
|
+
async function objectFile(ctx, cfb, e, d, location, plan) {
|
|
113
|
+
const run = ctx.run;
|
|
114
|
+
const native = e.children.find(c => c.kind === 'stream' && upper(c) === '\x01OLE10NATIVE');
|
|
115
|
+
if (d.pkg?.embedded && native) {
|
|
116
|
+
const pkg = d.pkg;
|
|
117
|
+
const source = cfb.source(native);
|
|
118
|
+
const start = 4 + pkg.dataAt;
|
|
119
|
+
const length = Math.max(0, Math.min(pkg.size, native.size - start));
|
|
120
|
+
const decision = await (0, contained_1.decideContained)(ctx, { name: pkg.fileName || undefined, location, chunks: () => (0, io_1.rangeChunks)(source, start, length) });
|
|
121
|
+
if (decision?.replacement) {
|
|
122
|
+
const all = await cfb.bytes(native);
|
|
123
|
+
plan.replace.set(native, await (0, contained_1.spillOf)(run, (0, oleobj_1.sizedStream)((0, oleobj_1.replaceSized)(all.subarray(4), pkg.dataAt, pkg.size, await (0, io_1.readAll)(decision.replacement)))));
|
|
124
|
+
}
|
|
125
|
+
return decision;
|
|
126
|
+
}
|
|
127
|
+
const written = new io_1.SpillSink(run.temp, run.threshold);
|
|
128
|
+
run.held.push(written);
|
|
129
|
+
await (0, cfb_writer_1.writeCfb)((0, rewrite_cfb_1.subtree)(cfb, e), written, run.deadline);
|
|
130
|
+
await written.close();
|
|
131
|
+
const decision = await (0, contained_1.decideContained)(ctx, { name: d.userType || d.progId || undefined, location, chunks: () => written.read() });
|
|
132
|
+
const replacement = decision?.replacement;
|
|
133
|
+
if (!replacement)
|
|
134
|
+
return decision;
|
|
135
|
+
const source = replacement.source();
|
|
136
|
+
run.sources.push(source);
|
|
137
|
+
let graft;
|
|
138
|
+
try {
|
|
139
|
+
graft = await cfb_1.CompoundFile.open(source, ctx.settings, run.deadline);
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
if (!(err instanceof errors_1.StopError))
|
|
143
|
+
throw err;
|
|
144
|
+
ctx.removal(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.PluginFailed, location, { reason: 'its output is not a compound file' });
|
|
145
|
+
return { keep: false };
|
|
146
|
+
}
|
|
147
|
+
plan.grafts.set(e, graft);
|
|
148
|
+
// The output check writes the storage out again, so it expects the bytes this writer makes of the graft.
|
|
149
|
+
const again = new io_1.SpillSink(run.temp, run.threshold);
|
|
150
|
+
run.held.push(again);
|
|
151
|
+
await (0, cfb_writer_1.writeCfb)((0, rewrite_cfb_1.subtree)(graft, graft.root), again, run.deadline);
|
|
152
|
+
await again.close();
|
|
153
|
+
const expected = run.kept.get(replacement.digest());
|
|
154
|
+
if (expected)
|
|
155
|
+
run.kept.set(again.digest(), expected);
|
|
156
|
+
return decision;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* One OLE object's storage: a control, a linked object, or an embedded one, with what it holds. Returns its findings,
|
|
160
|
+
* and a strip when a plugin removed the object. In the file the rewrite writes, `plan` is set and an embedded object
|
|
161
|
+
* goes to the plugins first. `given` is the verdict on an object whose container showed it to the plugins already.
|
|
162
|
+
*/
|
|
163
|
+
async function objectRules(ctx, cfb, e, location, plan, given) {
|
|
164
|
+
const d = await describeObject(cfb, e, location);
|
|
165
|
+
const data = { storage: location };
|
|
166
|
+
if (d.clsid)
|
|
167
|
+
data.clsid = d.clsid;
|
|
168
|
+
if (d.progId)
|
|
169
|
+
data.progId = (0, context_1.clip)(d.progId);
|
|
170
|
+
if (d.userType)
|
|
171
|
+
data.className = (0, context_1.clip)(d.userType);
|
|
172
|
+
if (d.control)
|
|
173
|
+
return [ctx.report(types_1.OfficeCategory.Control, types_1.OfficeDetail.ActiveX, location, d.controlName ? { ...data, name: (0, context_1.clip)(d.controlName) } : data)];
|
|
174
|
+
const found = [];
|
|
175
|
+
if (d.pkg) {
|
|
176
|
+
Object.assign(data, packageData(d.pkg));
|
|
177
|
+
if (!d.pkg.embedded && d.pkg.sourcePath)
|
|
178
|
+
found.push(ctx.external(types_1.OfficeDetail.Object, d.pkg.sourcePath, location, { storage: location }, 'package link'));
|
|
179
|
+
}
|
|
180
|
+
if (d.linked)
|
|
181
|
+
found.push(linkedObject(ctx, d, location, { storage: location, ...(d.clsid ? { clsid: d.clsid } : {}) }));
|
|
182
|
+
else {
|
|
183
|
+
const decision = given ?? (plan ? await objectFile(ctx, cfb, e, d, location, plan) : undefined);
|
|
184
|
+
found.push(ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.OleObject, location, data, (0, contained_1.presence)(decision)));
|
|
185
|
+
if (decision && !decision.keep)
|
|
186
|
+
found.push({ action: 'strip' });
|
|
187
|
+
}
|
|
188
|
+
if (d.equation)
|
|
189
|
+
found.push(ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Equation, location, data));
|
|
190
|
+
return found;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Reports a linked object. A URL moniker's target loads from outside the file whatever its text looks like: a
|
|
194
|
+
* CVE-2017-0199 sample puts a character with a stray high byte where the colon of https: goes.
|
|
195
|
+
*/
|
|
196
|
+
function linkedObject(ctx, d, location, data) {
|
|
197
|
+
if (d.target && !d.url)
|
|
198
|
+
return ctx.external(types_1.OfficeDetail.Object, d.target, location, data, 'linked object');
|
|
199
|
+
return ctx.report(types_1.OfficeCategory.External, types_1.OfficeDetail.Object, location, d.target ? { ...data, target: (0, context_1.clip)(d.target, 2000) } : data);
|
|
200
|
+
}
|
|
201
|
+
/** What a finding shows of a Packager object's file: its name and paths, and its size when the object holds it. */
|
|
202
|
+
function packageData(p) {
|
|
203
|
+
return { fileName: (0, context_1.clip)(p.fileName), sourcePath: (0, context_1.clip)(p.sourcePath, 2000), tempPath: (0, context_1.clip)(p.tempPath, 2000), ...(p.embedded ? { size: p.size } : {}) };
|
|
204
|
+
}
|