@patrickdk77/office-defuse 0.2.1 → 0.2.4
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 +82 -10
- package/dist/biff.d.ts +11 -0
- package/dist/biff.js +15 -0
- package/dist/contained.d.ts +5 -0
- package/dist/contained.js +1 -0
- package/dist/context.d.ts +10 -0
- package/dist/context.js +10 -0
- package/dist/engine.js +2 -0
- package/dist/fields.js +1 -1
- package/dist/findings.js +7 -1
- package/dist/forms.d.ts +51 -0
- package/dist/forms.js +316 -0
- package/dist/macros.d.ts +21 -0
- package/dist/macros.js +149 -0
- package/dist/objects.js +50 -12
- package/dist/ole.d.ts +3 -3
- package/dist/ole.js +55 -5
- package/dist/ooxml.d.ts +26 -0
- package/dist/ooxml.js +164 -10
- package/dist/ppt-writer.d.ts +27 -0
- package/dist/ppt-writer.js +105 -0
- package/dist/ppt.d.ts +5 -1
- package/dist/ppt.js +177 -46
- package/dist/rewrite-zip.js +2 -1
- package/dist/types.d.ts +44 -0
- package/dist/types.js +3 -0
- package/dist/vba.d.ts +78 -0
- package/dist/vba.js +378 -0
- package/dist/xls-writer.d.ts +34 -0
- package/dist/xls-writer.js +250 -0
- package/dist/xls.d.ts +9 -1
- package/dist/xls.js +126 -43
- package/dist/xml.d.ts +3 -0
- package/dist/xml.js +19 -1
- package/package.json +1 -1
package/dist/ppt.js
CHANGED
|
@@ -39,11 +39,15 @@ const cfb_1 = require("./cfb");
|
|
|
39
39
|
const contained_1 = require("./contained");
|
|
40
40
|
const context_1 = require("./context");
|
|
41
41
|
const errors_1 = require("./errors");
|
|
42
|
+
const forms_1 = require("./forms");
|
|
42
43
|
const io_1 = require("./io");
|
|
43
44
|
const links_1 = require("./links");
|
|
44
45
|
const objects_1 = require("./objects");
|
|
45
46
|
const offcrypto_1 = require("./offcrypto");
|
|
46
47
|
const oleobj_1 = require("./oleobj");
|
|
48
|
+
const plan_1 = require("./plan");
|
|
49
|
+
const ppt_writer_1 = require("./ppt-writer");
|
|
50
|
+
const rewrite_cfb_1 = require("./rewrite-cfb");
|
|
47
51
|
const types_1 = require("./types");
|
|
48
52
|
/** Record types of the PowerPoint binary format (MS-PPT 2.13.24). */
|
|
49
53
|
const T = {
|
|
@@ -71,29 +75,44 @@ const header = (b) => ({ version: b[0] & 0x0f, instance: (b[0] | (b[1] << 8)) >>
|
|
|
71
75
|
* Inspects a PowerPoint 97 to 2003 presentation. PowerPoint finds its objects through the persist directory that the
|
|
72
76
|
* current edit names. This reads every record at the top level of the stream as well as every object the persist
|
|
73
77
|
* directories name, so an object that one route hides is still read.
|
|
78
|
+
*
|
|
79
|
+
* With `plan`, what the strip findings name is removed by a full save of the stream: the persist objects the current
|
|
80
|
+
* edit reaches, written again with the records that go left out, a new persist directory and a new UserEditAtom.
|
|
74
81
|
*/
|
|
75
|
-
async function powerPointRules(ctx, cfb, stream) {
|
|
82
|
+
async function powerPointRules(ctx, cfb, stream, plan) {
|
|
76
83
|
const location = stream.name;
|
|
77
84
|
let source = cfb.source(stream);
|
|
78
85
|
let reader = new io_1.Reader(source, stream.size, 16384, 8);
|
|
79
86
|
const persist = new Map();
|
|
87
|
+
const edits = (0, ppt_writer_1.noPresentationEdits)();
|
|
80
88
|
const current = cfb.child(cfb.root, 'Current User');
|
|
89
|
+
let editAt = 0;
|
|
90
|
+
let session;
|
|
81
91
|
if (current?.kind === 'stream') {
|
|
82
92
|
const cu = await cfb.source(current).read(0, 24);
|
|
83
93
|
if (cu.length >= 20 && header(cu).type === T.currentUserAtom) {
|
|
84
|
-
|
|
94
|
+
editAt = u32(cu, 16);
|
|
95
|
+
await readPersist(reader, stream.size, editAt, persist, ctx.settings.deadline);
|
|
85
96
|
if (u32(cu, 12) === ENCRYPTED_TOKEN) {
|
|
86
|
-
|
|
97
|
+
const before = ctx.findings.length;
|
|
98
|
+
const opened = await decryptPresentation(ctx, reader, stream, editAt, persist);
|
|
99
|
+
source = opened.source;
|
|
100
|
+
session = opened.session;
|
|
87
101
|
reader = new io_1.Reader(source, stream.size, 16384, 8);
|
|
102
|
+
// The rewrite writes the presentation decrypted, so one VelvetSweatshop opens is rewritten when that finding strips.
|
|
103
|
+
if (ctx.findings.slice(before).some(f => f.category === types_1.OfficeCategory.Encrypted && f.detail === types_1.OfficeDetail.DefaultPassword && f.action === 'strip'))
|
|
104
|
+
edits.decrypt = true;
|
|
88
105
|
}
|
|
89
106
|
}
|
|
90
107
|
}
|
|
91
108
|
const storages = new Map();
|
|
92
|
-
/**
|
|
109
|
+
/** ExOleObjAtoms by the persist id of the storage they name: the object's type, 0 embedded, 1 linked, 2 a control, and the container that holds the atom. */
|
|
93
110
|
const roles = new Map();
|
|
94
|
-
|
|
111
|
+
/** VBAInfoAtoms by the persist id of the project's storage, with the VBAInfoContainer around each. */
|
|
112
|
+
const vba = new Map();
|
|
113
|
+
const containers = [];
|
|
95
114
|
const visited = new Set();
|
|
96
|
-
const walk = async (start, end, depth, siblings) => {
|
|
115
|
+
const walk = async (start, end, depth, siblings, parent) => {
|
|
97
116
|
for (let pos = start; pos + 8 <= end;) {
|
|
98
117
|
(0, errors_1.checkTime)(ctx.settings.deadline);
|
|
99
118
|
const h = header(await reader.read(pos, 8));
|
|
@@ -106,32 +125,87 @@ async function powerPointRules(ctx, cfb, stream) {
|
|
|
106
125
|
storages.set(pos, { offset: pos, header: h });
|
|
107
126
|
else if (h.type === T.exOleObjAtom && h.length >= 20) {
|
|
108
127
|
const b = await reader.read(pos + 8, 20);
|
|
109
|
-
roles.set(u32(b, 16), u32(b, 4));
|
|
128
|
+
roles.set(u32(b, 16), { type: u32(b, 4), container: parent });
|
|
129
|
+
}
|
|
130
|
+
else if (h.type === T.vbaInfoAtom && h.length >= 4) {
|
|
131
|
+
const id = u32(await reader.read(pos + 8, 4), 0);
|
|
132
|
+
vba.set(id, [...(vba.get(id) ?? []), parent]);
|
|
110
133
|
}
|
|
111
|
-
else if (h.type === T.vbaInfoAtom && h.length >= 4)
|
|
112
|
-
vba.add(u32(await reader.read(pos + 8, 4), 0));
|
|
113
134
|
if (h.version === 0x0f && depth < MAX_DEPTH) {
|
|
114
135
|
const kids = [];
|
|
115
|
-
await walk(pos + 8, bodyEnd, depth + 1, kids);
|
|
116
|
-
|
|
136
|
+
await walk(pos + 8, bodyEnd, depth + 1, kids, pos);
|
|
137
|
+
containers.push({ h, offset: pos, kids });
|
|
117
138
|
}
|
|
118
139
|
pos = bodyEnd;
|
|
119
140
|
}
|
|
120
141
|
};
|
|
121
142
|
// Every record at the top level, then each persist object that does not start on a record already read.
|
|
122
|
-
await walk(0, stream.size, 0, []);
|
|
143
|
+
await walk(0, stream.size, 0, [], -1);
|
|
123
144
|
for (const offset of persist.values()) {
|
|
124
145
|
if (visited.has(offset) || offset + 8 > stream.size)
|
|
125
146
|
continue;
|
|
126
147
|
const h = header(await reader.read(offset, 8));
|
|
127
|
-
await walk(offset, Math.min(stream.size, offset + 8 + h.length), 0, []);
|
|
148
|
+
await walk(offset, Math.min(stream.size, offset + 8 + h.length), 0, [], -1);
|
|
128
149
|
}
|
|
150
|
+
// The storages come first: a VBA project that stays keeps the actions that run its macros.
|
|
129
151
|
const byOffset = new Map([...persist].map(([id, offset]) => [offset, id]));
|
|
130
152
|
for (const s of storages.values()) {
|
|
131
153
|
const id = byOffset.get(s.offset);
|
|
132
|
-
const role = id !== undefined && vba.has(id) ? 'vba' : id !== undefined && roles.get(id) === 2 ? 'control' : 'object';
|
|
133
|
-
await storageRules(ctx, source, s, role, `${location}, offset ${s.offset}
|
|
154
|
+
const role = id !== undefined && vba.has(id) ? 'vba' : id !== undefined && roles.get(id)?.type === 2 ? 'control' : 'object';
|
|
155
|
+
const verdict = await storageRules(ctx, source, s, role, `${location}, offset ${s.offset}`, plan !== undefined);
|
|
156
|
+
if (id === undefined || verdict === 'keep')
|
|
157
|
+
continue;
|
|
158
|
+
if (verdict === 'remove') {
|
|
159
|
+
// The storage goes, and so does what names it: the VBAInfoContainer, or the object's container in ExObjList.
|
|
160
|
+
edits.dropPersist.add(id);
|
|
161
|
+
if (role === 'vba')
|
|
162
|
+
for (const c of vba.get(id) ?? [])
|
|
163
|
+
edits.remove.add(c);
|
|
164
|
+
else {
|
|
165
|
+
const holder = roles.get(id)?.container;
|
|
166
|
+
if (holder !== undefined && holder >= 0)
|
|
167
|
+
edits.remove.add(holder);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
else
|
|
171
|
+
edits.replace.set(s.offset, verdict.body);
|
|
172
|
+
}
|
|
173
|
+
const links = new Map();
|
|
174
|
+
const actions = [];
|
|
175
|
+
for (const c of containers)
|
|
176
|
+
await containerRules(ctx, reader, c.h, c.offset, c.kids, location, edits, links, actions);
|
|
177
|
+
// An action that opens a hyperlink that went does nothing.
|
|
178
|
+
for (const a of actions) {
|
|
179
|
+
if (a.action !== 4 || !links.has(a.hyperlink) || edits.replace.has(a.offset))
|
|
180
|
+
continue;
|
|
181
|
+
const body = Uint8Array.from(await reader.read(a.offset + 8, 16));
|
|
182
|
+
body[8] = 0;
|
|
183
|
+
edits.replace.set(a.offset, body);
|
|
134
184
|
}
|
|
185
|
+
if (!plan || !current || !(0, ppt_writer_1.hasPresentationEdits)(edits))
|
|
186
|
+
return;
|
|
187
|
+
// An encrypted presentation's pictures and summary are encrypted too, in streams this rewrite does not decrypt. It
|
|
188
|
+
// is left as it was, and the check of the output says what it still holds.
|
|
189
|
+
if (session !== undefined && ['Pictures', 'EncryptedSummary'].some(n => (cfb.child(cfb.root, n)?.size ?? 0) > 0))
|
|
190
|
+
return;
|
|
191
|
+
if (session !== undefined) {
|
|
192
|
+
edits.decrypt = true;
|
|
193
|
+
edits.dropPersist.add(session);
|
|
194
|
+
}
|
|
195
|
+
const userEdit = header(await reader.read(editAt, 8));
|
|
196
|
+
const body = await reader.read(editAt + 8, Math.min(userEdit.length, 64));
|
|
197
|
+
if (userEdit.type !== T.userEditAtom || body.length < 28)
|
|
198
|
+
return;
|
|
199
|
+
const sink = new io_1.SpillSink(ctx.run.temp, ctx.run.threshold);
|
|
200
|
+
ctx.run.held.push(sink);
|
|
201
|
+
const newEdit = await (0, ppt_writer_1.rewritePresentation)(source, stream.size, persist, { body }, edits, sink, ctx.settings.deadline);
|
|
202
|
+
await sink.close();
|
|
203
|
+
plan.replace.set(stream, sink);
|
|
204
|
+
const user = Uint8Array.from(await cfb.bytes(current));
|
|
205
|
+
user.set([newEdit & 0xff, (newEdit >>> 8) & 0xff, (newEdit >>> 16) & 0xff, (newEdit >>> 24) & 0xff], 16);
|
|
206
|
+
if (edits.decrypt)
|
|
207
|
+
user.set([0x5f, 0xc0, 0x91, 0xe3], 12);
|
|
208
|
+
plan.replace.set(current, await (0, contained_1.spillOf)(ctx.run, user));
|
|
135
209
|
}
|
|
136
210
|
/**
|
|
137
211
|
* The PowerPoint Document stream of an encrypted presentation, decrypted (MS-PPT 2.3.7). The current edit names the
|
|
@@ -142,7 +216,8 @@ async function powerPointRules(ctx, cfb, stream) {
|
|
|
142
216
|
async function decryptPresentation(ctx, reader, stream, edit, persist) {
|
|
143
217
|
const required = (reason) => new errors_1.StopError(types_1.OfficeCategory.Encrypted, types_1.OfficeDetail.PasswordRequired, `The presentation is encrypted: ${reason}`, { stream: stream.name, reason });
|
|
144
218
|
const u = await reader.read(edit, 40);
|
|
145
|
-
const
|
|
219
|
+
const sessionId = u.length >= 40 && header(u).type === T.userEditAtom ? u32(u, 36) : -1;
|
|
220
|
+
const sessionAt = persist.get(sessionId);
|
|
146
221
|
if (sessionAt === undefined || sessionAt + 8 > stream.size)
|
|
147
222
|
throw required('the current edit names no CryptSession10Container');
|
|
148
223
|
const h = header(await reader.read(sessionAt, 8));
|
|
@@ -162,7 +237,7 @@ async function decryptPresentation(ctx, reader, stream, edit, persist) {
|
|
|
162
237
|
copy.set(head, offset);
|
|
163
238
|
copy.set(rc4.crypt(copy.subarray(offset + 8, offset + 8 + length)), offset + 8);
|
|
164
239
|
}
|
|
165
|
-
return (0, io_1.bufferSource)(copy);
|
|
240
|
+
return { source: (0, io_1.bufferSource)(copy), session: sessionId };
|
|
166
241
|
}
|
|
167
242
|
/** Reads the persist directories of the current edit and every edit before it. Newer entries win. */
|
|
168
243
|
async function readPersist(reader, size, start, persist, deadline) {
|
|
@@ -190,10 +265,15 @@ async function readPersist(reader, size, start, persist, deadline) {
|
|
|
190
265
|
edit = u32(u, 16);
|
|
191
266
|
}
|
|
192
267
|
}
|
|
193
|
-
/**
|
|
194
|
-
|
|
268
|
+
/**
|
|
269
|
+
* One ExOleObjStg: a VBA project, a control or an OLE object, stored as a compound file that may be compressed. With
|
|
270
|
+
* `rewrite`, a VBA project goes to the script plugins and stays, without its compiled code, when they pass it; a
|
|
271
|
+
* Forms 2.0 control stays; an object stays as the file plugins left it. Anything a strip finding names goes.
|
|
272
|
+
*/
|
|
273
|
+
async function storageRules(ctx, source, s, role, location, rewrite) {
|
|
274
|
+
const compressed = s.header.instance === 1;
|
|
195
275
|
let inner;
|
|
196
|
-
if (
|
|
276
|
+
if (compressed) {
|
|
197
277
|
const raw = await source.read(s.offset + 8, s.header.length);
|
|
198
278
|
const declared = raw.length >= 4 ? u32(raw, 0) : 0;
|
|
199
279
|
if (declared > ctx.settings.decompressedBytes)
|
|
@@ -203,8 +283,7 @@ async function storageRules(ctx, source, s, role, location) {
|
|
|
203
283
|
bytes = zlib.inflateSync(raw.subarray(4), { maxOutputLength: Math.max(1, declared) });
|
|
204
284
|
}
|
|
205
285
|
catch {
|
|
206
|
-
unreadable(ctx, role, location, 'its compressed data does not inflate to its declared size');
|
|
207
|
-
return;
|
|
286
|
+
return unreadable(ctx, role, location, 'its compressed data does not inflate to its declared size');
|
|
208
287
|
}
|
|
209
288
|
inner = { size: async () => bytes.length, read: async (offset, length) => bytes.subarray(offset, offset + length) };
|
|
210
289
|
}
|
|
@@ -223,28 +302,67 @@ async function storageRules(ctx, source, s, role, location) {
|
|
|
223
302
|
catch (e) {
|
|
224
303
|
if (!(e instanceof errors_1.StopError) || e.category !== types_1.OfficeCategory.Container)
|
|
225
304
|
throw e;
|
|
226
|
-
unreadable(ctx, role, location, `its storage does not read: ${e.message}`);
|
|
227
|
-
return;
|
|
305
|
+
return unreadable(ctx, role, location, `its storage does not read: ${e.message}`);
|
|
228
306
|
}
|
|
229
307
|
(0, objects_1.nestedNotes)(ctx, cfb, location);
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
308
|
+
const strips = ctx.strips;
|
|
309
|
+
if (role === 'control') {
|
|
310
|
+
const data = { storage: location, ...(cfb.root.clsid === '00000000-0000-0000-0000-000000000000' ? {} : { clsid: cfb.root.clsid }) };
|
|
311
|
+
const problem = await (0, forms_1.controlStorageProblem)(cfb, cfb.root, forms_1.OOXML_CONTROL_STREAMS);
|
|
312
|
+
if (problem)
|
|
313
|
+
ctx.report(types_1.OfficeCategory.Control, types_1.OfficeDetail.ActiveX, location, { ...data, reason: (0, context_1.clip)(problem) });
|
|
314
|
+
else
|
|
315
|
+
ctx.report(types_1.OfficeCategory.Control, types_1.OfficeDetail.FormsControl, location, data);
|
|
316
|
+
// What the storage holds besides the control gets the rules any compound file gets.
|
|
317
|
+
await (0, objects_1.compoundRules)(ctx, cfb, location, false);
|
|
318
|
+
return ctx.strips > strips ? 'remove' : 'keep';
|
|
319
|
+
}
|
|
320
|
+
if (role === 'vba') {
|
|
321
|
+
// The project's storage is the root of the compound file, so the rules give it a plan of its own.
|
|
322
|
+
const own = new plan_1.CompoundPlan();
|
|
323
|
+
await (0, objects_1.compoundRules)(ctx, cfb, location, false, rewrite ? own : undefined);
|
|
324
|
+
const project = cfb.child(cfb.root, 'VBA');
|
|
325
|
+
if (!rewrite || !project || own.drop.has(project))
|
|
326
|
+
return ctx.strips > strips ? 'remove' : 'keep';
|
|
327
|
+
// The project stays: written again without its compiled code and whatever else its rules removed.
|
|
328
|
+
if (ctx.strips === strips && !own.replace.size && !own.drop.size)
|
|
329
|
+
return 'keep';
|
|
330
|
+
return { body: await storageBody(ctx, cfb, own, compressed) };
|
|
331
|
+
}
|
|
332
|
+
// An object's storage goes to the plugins as the compound file it is, and stays as they leave it.
|
|
234
333
|
const size = await inner.size();
|
|
235
|
-
const decision =
|
|
236
|
-
await (0, objects_1.compoundRules)(ctx, cfb, location,
|
|
334
|
+
const decision = await (0, contained_1.decideContained)(ctx, { location, chunks: () => (0, io_1.chunksOf)(inner, size) });
|
|
335
|
+
await (0, objects_1.compoundRules)(ctx, cfb, location, true, undefined, decision);
|
|
336
|
+
if ((decision && !decision.keep) || ctx.strips > strips)
|
|
337
|
+
return 'remove';
|
|
338
|
+
if (decision?.replacement) {
|
|
339
|
+
const bytes = await (0, io_1.readAll)(decision.replacement);
|
|
340
|
+
return { body: compressed ? Buffer.concat([Buffer.from(le32(bytes.length)), zlib.deflateSync(bytes)]) : bytes };
|
|
341
|
+
}
|
|
342
|
+
return 'keep';
|
|
237
343
|
}
|
|
344
|
+
/** An ExOleObjStg's data for a compound file written with `plan`: deflated after its size when it was compressed. */
|
|
345
|
+
async function storageBody(ctx, cfb, plan, compressed) {
|
|
346
|
+
const sink = (0, io_1.bufferSink)();
|
|
347
|
+
await (0, rewrite_cfb_1.writeCompound)(ctx.run, cfb, plan, sink);
|
|
348
|
+
const bytes = sink.result();
|
|
349
|
+
return compressed ? Buffer.concat([Buffer.from(le32(bytes.length)), zlib.deflateSync(bytes)]) : bytes;
|
|
350
|
+
}
|
|
351
|
+
const le32 = (n) => [n & 0xff, (n >>> 8) & 0xff, (n >>> 16) & 0xff, (n >>> 24) & 0xff];
|
|
238
352
|
function unreadable(ctx, role, location, reason) {
|
|
239
|
-
|
|
240
|
-
ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.VbaProject, location, { storage: location, reason })
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
353
|
+
const r = role === 'vba'
|
|
354
|
+
? ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.VbaProject, location, { storage: location, reason })
|
|
355
|
+
: role === 'control'
|
|
356
|
+
? ctx.report(types_1.OfficeCategory.Control, types_1.OfficeDetail.ActiveX, location, { storage: location, reason })
|
|
357
|
+
: ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.OleObject, location, { storage: location, reason });
|
|
358
|
+
return r.action === 'strip' ? 'remove' : 'keep';
|
|
245
359
|
}
|
|
246
|
-
/**
|
|
247
|
-
|
|
360
|
+
/**
|
|
361
|
+
* The rules of one container record, from its children: actions, hyperlinks and linked media. What strips goes in the
|
|
362
|
+
* edits: an action becomes no action, a hyperlink goes, and a linked file's path is emptied. `links` collects the ids
|
|
363
|
+
* of the hyperlinks that go, and `actions` every action, for the actions that open those.
|
|
364
|
+
*/
|
|
365
|
+
async function containerRules(ctx, reader, h, offset, kids, stream, edits, links, actions) {
|
|
248
366
|
const location = `${stream}, offset ${offset}`;
|
|
249
367
|
const strings = async () => {
|
|
250
368
|
const out = new Map();
|
|
@@ -257,25 +375,38 @@ async function containerRules(ctx, reader, h, offset, kids, stream) {
|
|
|
257
375
|
const atom = kids.find(k => k.h.type === T.interactiveInfoAtom && k.h.length >= 16);
|
|
258
376
|
if (!atom)
|
|
259
377
|
return;
|
|
260
|
-
const
|
|
378
|
+
const body = Uint8Array.from(await reader.read(atom.offset + 8, 16));
|
|
379
|
+
const action = body[8];
|
|
380
|
+
actions.push({ offset: atom.offset, hyperlink: u32(body, 4), action });
|
|
261
381
|
const name = (await strings()).get(2) ?? '';
|
|
262
382
|
const data = { trigger: h.instance === 1 ? 'mouse over' : 'click', ...(name ? { action: (0, context_1.clip)(name) } : {}) };
|
|
383
|
+
let r;
|
|
263
384
|
if (action === 2)
|
|
264
|
-
ctx.report(types_1.OfficeCategory.Action, types_1.OfficeDetail.RunProgram, location, data);
|
|
385
|
+
r = ctx.report(types_1.OfficeCategory.Action, types_1.OfficeDetail.RunProgram, location, data);
|
|
386
|
+
// A macro of a VBA project that stays keeps its action.
|
|
265
387
|
else if (action === 1)
|
|
266
|
-
ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.Link, location, data);
|
|
388
|
+
r = ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.Link, location, data, (0, context_1.ownMacro)(ctx.run, name));
|
|
267
389
|
else if (action === 5)
|
|
268
|
-
ctx.report(types_1.OfficeCategory.Action, types_1.OfficeDetail.OleVerb, location, data);
|
|
390
|
+
r = ctx.report(types_1.OfficeCategory.Action, types_1.OfficeDetail.OleVerb, location, data);
|
|
391
|
+
if (r?.action === 'strip') {
|
|
392
|
+
body[8] = 0;
|
|
393
|
+
edits.replace.set(atom.offset, body);
|
|
394
|
+
}
|
|
269
395
|
}
|
|
270
396
|
else if (h.type === T.exHyperlink) {
|
|
271
397
|
const s = await strings();
|
|
272
398
|
const target = s.get(1) ?? '';
|
|
273
|
-
|
|
274
|
-
|
|
399
|
+
const atom = kids.find(k => k.h.type === T.exHyperlinkAtom && k.h.length >= 4);
|
|
400
|
+
if (target.trim() && !(0, links_1.isFragment)(target) && ctx.link(target, location, { source: 'PowerPoint hyperlink' }).action === 'strip') {
|
|
401
|
+
edits.remove.add(offset);
|
|
402
|
+
if (atom)
|
|
403
|
+
links.set(u32(await reader.read(atom.offset + 8, 4), 0), offset);
|
|
404
|
+
}
|
|
275
405
|
}
|
|
276
406
|
else if (MEDIA_LINKS.has(h.type)) {
|
|
277
407
|
const path = (await strings()).get(0) ?? '';
|
|
278
|
-
|
|
279
|
-
|
|
408
|
+
const string = kids.find(k => k.h.type === T.cString && k.h.instance === 0);
|
|
409
|
+
if (path.trim() && ctx.external(types_1.OfficeDetail.Other, path, location, { source: 'linked sound or movie' }, 'linked media').action === 'strip' && string)
|
|
410
|
+
edits.replace.set(string.offset, new Uint8Array(0));
|
|
280
411
|
}
|
|
281
412
|
}
|
package/dist/rewrite-zip.js
CHANGED
|
@@ -281,7 +281,8 @@ async function writeOoxml(run, zip, plan, graph, format, limits, sink) {
|
|
|
281
281
|
for (const [k, p] of graph.parts) {
|
|
282
282
|
const removed = removedIds.get(k);
|
|
283
283
|
const own = plan.parts.get(p.entry.name);
|
|
284
|
-
|
|
284
|
+
// An empty part, such as the presentation part PowerPoint writes into an add-in, refers to nothing.
|
|
285
|
+
if (drops.has(k) || sheets.has(k) || plan.replace.has(k) || !(0, ooxml_1.isXmlType)(p.contentType) || p.entry.size === 0 || (!removed && !own && !xpsDrops))
|
|
285
286
|
continue;
|
|
286
287
|
const rules = {
|
|
287
288
|
element(el) {
|
package/dist/types.d.ts
CHANGED
|
@@ -79,6 +79,7 @@ export declare enum OfficeDetail {
|
|
|
79
79
|
UnsupportedAlgorithm = "UNSUPPORTED_ALGORITHM",
|
|
80
80
|
XorObfuscation = "XOR_OBFUSCATION",
|
|
81
81
|
VbaProject = "VBA_PROJECT",
|
|
82
|
+
CompiledCode = "COMPILED_CODE",
|
|
82
83
|
XlmMacrosheet = "XLM_MACROSHEET",
|
|
83
84
|
WordBasic = "WORDBASIC",
|
|
84
85
|
OdfBasic = "ODF_BASIC",
|
|
@@ -132,6 +133,8 @@ export declare enum OfficeDetail {
|
|
|
132
133
|
PluginFailed = "PLUGIN_FAILED",
|
|
133
134
|
PluginRemoved = "PLUGIN_REMOVED",
|
|
134
135
|
ActiveX = "ACTIVEX",
|
|
136
|
+
FormsControl = "FORMS_CONTROL",
|
|
137
|
+
ControlPicture = "CONTROL_PICTURE",
|
|
135
138
|
VerificationFailed = "VERIFICATION_FAILED"
|
|
136
139
|
}
|
|
137
140
|
export type OfficeFindingAction = 'reject' | 'strip' | 'info';
|
|
@@ -293,6 +296,41 @@ export interface ContainedFilePlugin {
|
|
|
293
296
|
/** Writes the bytes to keep. Returning 'removed' drops the file after all. */
|
|
294
297
|
process(file: ContainedFile, sink: ByteSink, context: PluginContext): Promise<'scrubbed' | 'passed' | 'removed' | ContainedFileResult>;
|
|
295
298
|
}
|
|
299
|
+
/** A type library or project a VBA project refers to, as its dir stream lists it. */
|
|
300
|
+
export interface VbaReferenceInfo {
|
|
301
|
+
name: string;
|
|
302
|
+
kind: 'registered' | 'control' | 'project';
|
|
303
|
+
/** A type library's LibidReference, such as `*\G{...}#2.0#0#path#description`, or a project's path. */
|
|
304
|
+
libid: string;
|
|
305
|
+
}
|
|
306
|
+
/** A VBA module, as office-defuse shows it to a script plugin. */
|
|
307
|
+
export interface ContainedScript {
|
|
308
|
+
/** The module's source, decoded with the project's code page. */
|
|
309
|
+
text: string;
|
|
310
|
+
/** "vba-" and the module's kind: "vba-document", "vba-standard", "vba-class" or "vba-form". */
|
|
311
|
+
trigger: string;
|
|
312
|
+
location: string;
|
|
313
|
+
/** The module's name. */
|
|
314
|
+
fieldName: string;
|
|
315
|
+
language: 'vba';
|
|
316
|
+
form: 'office';
|
|
317
|
+
/** The references of the module's project. office-defuse gives them with the project's first module only. */
|
|
318
|
+
references?: VbaReferenceInfo[];
|
|
319
|
+
}
|
|
320
|
+
export interface ScriptPlugin {
|
|
321
|
+
kind: 'script';
|
|
322
|
+
name: string;
|
|
323
|
+
accepts(script: ContainedScript): boolean | Promise<boolean>;
|
|
324
|
+
/**
|
|
325
|
+
* 'passed' keeps the module. Anything else removes the VBA project, since office-defuse writes no module a plugin
|
|
326
|
+
* rewrote. `findings` go into the report at the module's location.
|
|
327
|
+
*/
|
|
328
|
+
process(script: ContainedScript): Promise<{
|
|
329
|
+
result: 'scrubbed' | 'passed' | 'removed';
|
|
330
|
+
text?: string;
|
|
331
|
+
findings?: DefuseFinding[];
|
|
332
|
+
}>;
|
|
333
|
+
}
|
|
296
334
|
export interface OfficeActionOverride {
|
|
297
335
|
category: OfficeCategory;
|
|
298
336
|
detail?: OfficeDetail;
|
|
@@ -323,6 +361,12 @@ export interface OfficeOptions {
|
|
|
323
361
|
scoreBands?: OfficeScoreBands;
|
|
324
362
|
/** Plugins that decide about the files inside the file: embedded objects and packages, OLE parts and attachments. */
|
|
325
363
|
filePlugins?: ContainedFilePlugin[];
|
|
364
|
+
/**
|
|
365
|
+
* Plugins that decide about each module of a VBA project in a compound file or an OOXML package. The first plugin that
|
|
366
|
+
* accepts a module decides it. The project stays when every module passed, written without its compiled code so
|
|
367
|
+
* Office compiles the source the plugins read. Otherwise it goes, as it does without plugins.
|
|
368
|
+
*/
|
|
369
|
+
scriptPlugins?: ScriptPlugin[];
|
|
326
370
|
/** Directory for temporary files. Defaults to os.tmpdir(). */
|
|
327
371
|
tempDir?: string;
|
|
328
372
|
/**
|
package/dist/types.js
CHANGED
|
@@ -90,6 +90,7 @@ var OfficeDetail;
|
|
|
90
90
|
OfficeDetail["XorObfuscation"] = "XOR_OBFUSCATION";
|
|
91
91
|
// MACRO
|
|
92
92
|
OfficeDetail["VbaProject"] = "VBA_PROJECT";
|
|
93
|
+
OfficeDetail["CompiledCode"] = "COMPILED_CODE";
|
|
93
94
|
OfficeDetail["XlmMacrosheet"] = "XLM_MACROSHEET";
|
|
94
95
|
OfficeDetail["WordBasic"] = "WORDBASIC";
|
|
95
96
|
OfficeDetail["OdfBasic"] = "ODF_BASIC";
|
|
@@ -151,6 +152,8 @@ var OfficeDetail;
|
|
|
151
152
|
OfficeDetail["PluginRemoved"] = "PLUGIN_REMOVED";
|
|
152
153
|
// CONTROL
|
|
153
154
|
OfficeDetail["ActiveX"] = "ACTIVEX";
|
|
155
|
+
OfficeDetail["FormsControl"] = "FORMS_CONTROL";
|
|
156
|
+
OfficeDetail["ControlPicture"] = "CONTROL_PICTURE";
|
|
154
157
|
// PROCESSING
|
|
155
158
|
OfficeDetail["VerificationFailed"] = "VERIFICATION_FAILED";
|
|
156
159
|
})(OfficeDetail || (exports.OfficeDetail = OfficeDetail = {}));
|
package/dist/vba.d.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { CfbEntry, CompoundFile } from './cfb';
|
|
2
|
+
/**
|
|
3
|
+
* A VBA project as MS-OVBA stores one: a VBA storage with a dir stream that lists the modules, and a stream per module
|
|
4
|
+
* that holds the module's compiled p-code and then its compressed source. Office runs the p-code when the same VBA
|
|
5
|
+
* version compiled it, so the p-code can do something other than the source says (VBA stomping). A project kept after
|
|
6
|
+
* its source was checked is written without the p-code, and with the source compressed again, so Office compiles the
|
|
7
|
+
* text that was checked.
|
|
8
|
+
*/
|
|
9
|
+
export declare class VbaError extends Error {
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Decompresses an MS-OVBA CompressedContainer (2.4.1.3.1). Throws VbaError when a copy token reaches before its chunk,
|
|
13
|
+
* a chunk decompresses past 4096 bytes, or the output passes `limit`. A chunk header without its signature bits is
|
|
14
|
+
* read like any other, and fewer than 3 bytes at the end are taken as padding: Office opens files written that way.
|
|
15
|
+
*/
|
|
16
|
+
export declare function decompress(data: Uint8Array, limit?: number): Uint8Array;
|
|
17
|
+
/**
|
|
18
|
+
* Compresses `data` into an MS-OVBA CompressedContainer (2.4.1.3.6). The longest match comes from a hash chain of
|
|
19
|
+
* the positions in the chunk. Throws VbaError for a last chunk that compresses past 4096 bytes, which MS-OVBA can only
|
|
20
|
+
* write as a raw chunk padded to 4096 bytes, with padding Office would read as source.
|
|
21
|
+
*/
|
|
22
|
+
export declare function compress(data: Uint8Array): Uint8Array;
|
|
23
|
+
/** One record of the dir stream (MS-OVBA 2.3.4.2), as read. */
|
|
24
|
+
interface DirRecord {
|
|
25
|
+
id: number;
|
|
26
|
+
data: Uint8Array;
|
|
27
|
+
}
|
|
28
|
+
/** A module of a VBA project. */
|
|
29
|
+
export interface VbaModule {
|
|
30
|
+
name: string;
|
|
31
|
+
/**
|
|
32
|
+
* What the PROJECT stream says the module is: a document's or a sheet's module, a code module, a class module or a
|
|
33
|
+
* user form. A module the PROJECT stream leaves out is a code or class module as its dir record says.
|
|
34
|
+
*/
|
|
35
|
+
kind: 'document' | 'standard' | 'class' | 'form';
|
|
36
|
+
stream: CfbEntry;
|
|
37
|
+
/** Where the compressed source starts in the stream. The bytes before it are p-code. */
|
|
38
|
+
offset: number;
|
|
39
|
+
/** The source as stored, in the project's code page. */
|
|
40
|
+
bytes: Uint8Array;
|
|
41
|
+
source: string;
|
|
42
|
+
}
|
|
43
|
+
/** A type library or project the VBA project refers to (MS-OVBA 2.3.4.2.2). */
|
|
44
|
+
export interface VbaReference {
|
|
45
|
+
name: string;
|
|
46
|
+
kind: 'registered' | 'control' | 'project';
|
|
47
|
+
/** The library's LibidReference (2.1.1.8), or for a project reference its absolute path (2.1.1.12). */
|
|
48
|
+
libid: string;
|
|
49
|
+
}
|
|
50
|
+
export interface VbaProject {
|
|
51
|
+
storage: CfbEntry;
|
|
52
|
+
dir: CfbEntry;
|
|
53
|
+
codePage: number;
|
|
54
|
+
modules: VbaModule[];
|
|
55
|
+
references: VbaReference[];
|
|
56
|
+
records: DirRecord[];
|
|
57
|
+
/** The MD5 of the references and the modules' names, kinds and sources: what a plugin's verdict is about. */
|
|
58
|
+
digest: string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Reads the project in `vba`, the VBA storage: the dir stream, the references, and each module's source. Throws
|
|
62
|
+
* VbaError when a stream the dir names is missing or a source does not decompress, which leaves nothing to check.
|
|
63
|
+
*/
|
|
64
|
+
export declare function readProject(cfb: CompoundFile, vba: CfbEntry, limit?: number): Promise<VbaProject>;
|
|
65
|
+
/** The `_VBA_PROJECT` stream of a project no VBA version compiled, which makes Office compile the source. */
|
|
66
|
+
export declare const PURGED_VBA_PROJECT: Uint8Array<ArrayBuffer>;
|
|
67
|
+
/**
|
|
68
|
+
* The project's streams without p-code, as Mandiant's OfficePurge writes them: each module stream holds only its
|
|
69
|
+
* source, compressed again; the dir stream says each source starts at 0; `_VBA_PROJECT` names no VBA version. The
|
|
70
|
+
* `__SRP_` caches and anything else in the VBA storage that the dir does not name go.
|
|
71
|
+
*/
|
|
72
|
+
export declare function purge(project: VbaProject): {
|
|
73
|
+
replace: Map<CfbEntry, Uint8Array>;
|
|
74
|
+
drop: CfbEntry[];
|
|
75
|
+
};
|
|
76
|
+
/** Whether the project is as `purge` writes it, so writing it again changes nothing. */
|
|
77
|
+
export declare function isPurged(cfb: CompoundFile, project: VbaProject): Promise<boolean>;
|
|
78
|
+
export {};
|