@patrickdk77/office-defuse 0.1.1 → 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 +197 -42
- package/dist/cfb-writer.d.ts +25 -0
- package/dist/cfb-writer.js +314 -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 +107 -12
- package/dist/context.js +101 -24
- package/dist/engine.d.ts +12 -1
- package/dist/engine.js +276 -31
- package/dist/errors.d.ts +17 -0
- package/dist/errors.js +27 -1
- package/dist/fields.d.ts +20 -13
- package/dist/fields.js +65 -23
- package/dist/findings.d.ts +2 -1
- package/dist/findings.js +17 -1
- 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 +3 -2
- package/dist/msg.js +172 -18
- package/dist/objects.d.ts +8 -5
- package/dist/objects.js +90 -20
- package/dist/odf.d.ts +65 -1
- package/dist/odf.js +181 -64
- package/dist/offcrypto.d.ts +24 -1
- package/dist/offcrypto.js +308 -69
- package/dist/ole.js +17 -5
- package/dist/oleobj.d.ts +11 -0
- package/dist/oleobj.js +20 -3
- package/dist/ooxml.d.ts +81 -0
- package/dist/ooxml.js +342 -81
- 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.js +45 -5
- 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.js +214 -41
- 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 +43 -6
- package/dist/types.js +14 -0
- package/dist/vsd.d.ts +13 -0
- package/dist/vsd.js +689 -0
- package/dist/word.js +51 -17
- package/dist/xls.js +38 -4
- 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 +4 -1
package/dist/rtf.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.inspectRtf = inspectRtf;
|
|
4
4
|
const cfb_1 = require("./cfb");
|
|
5
|
+
const contained_1 = require("./contained");
|
|
5
6
|
const context_1 = require("./context");
|
|
6
7
|
const errors_1 = require("./errors");
|
|
7
8
|
const fields_1 = require("./fields");
|
|
@@ -43,6 +44,8 @@ const MAX_WORD = 250;
|
|
|
43
44
|
const CHUNK = 262144;
|
|
44
45
|
/** Collected text other than an object's data is short: a class name, a template path, a link. */
|
|
45
46
|
const MAX_TEXT = 4096;
|
|
47
|
+
/** The pictures RTF holds as whole files of their own, by the control word that names their type. */
|
|
48
|
+
const PICTURE_TYPES = { pngblip: 'image/png', jpegblip: 'image/jpeg', emfblip: 'image/emf' };
|
|
46
49
|
/** An object's data as Word decodes its hex digits, with the quirks rtfobj documents. */
|
|
47
50
|
class HexData {
|
|
48
51
|
limit;
|
|
@@ -100,7 +103,9 @@ const isDigit = (c) => c >= 0x30 && c <= 0x39;
|
|
|
100
103
|
*/
|
|
101
104
|
async function inspectRtf(ctx, source, size) {
|
|
102
105
|
ctx.format = { format: 'rtf', mimeType: 'application/rtf' };
|
|
103
|
-
const
|
|
106
|
+
const cuts = [];
|
|
107
|
+
ctx.run.output = { kind: 'rtf', size, cuts };
|
|
108
|
+
const reader = new RtfReader(ctx, size, cuts);
|
|
104
109
|
for (let pos = 0; pos < size; pos += CHUNK) {
|
|
105
110
|
(0, errors_1.checkTime)(ctx.settings.deadline);
|
|
106
111
|
const chunk = await source.read(pos, Math.min(CHUNK, size - pos));
|
|
@@ -111,6 +116,7 @@ async function inspectRtf(ctx, source, size) {
|
|
|
111
116
|
class RtfReader {
|
|
112
117
|
ctx;
|
|
113
118
|
size;
|
|
119
|
+
cuts;
|
|
114
120
|
frames;
|
|
115
121
|
fields;
|
|
116
122
|
depth = 0;
|
|
@@ -130,11 +136,30 @@ class RtfReader {
|
|
|
130
136
|
decoded = 0;
|
|
131
137
|
/** Objects whose groups closed, read in order once the chunk is done. */
|
|
132
138
|
pending = [];
|
|
133
|
-
|
|
139
|
+
/** The offsets of the braces of the groups open now. */
|
|
140
|
+
groups = [];
|
|
141
|
+
/** Where the control word being read ends. */
|
|
142
|
+
wordEnd = 0;
|
|
143
|
+
fieldCount = 0;
|
|
144
|
+
/** The fldinst groups of each field, by the field's number, which go when a finding strips the field. */
|
|
145
|
+
codes = new Map();
|
|
146
|
+
/** Pictures go to the plugins only when there are any, or expected verdicts to check, so only then are they decoded. */
|
|
147
|
+
pictures;
|
|
148
|
+
pendingPictures = [];
|
|
149
|
+
constructor(ctx, size,
|
|
150
|
+
/** Where the rewrite cuts, which the rules fill in as their findings strip. */
|
|
151
|
+
cuts) {
|
|
134
152
|
this.ctx = ctx;
|
|
135
153
|
this.size = size;
|
|
136
|
-
this.
|
|
137
|
-
this.
|
|
154
|
+
this.cuts = cuts;
|
|
155
|
+
this.frames = [{ kind: 'document', depth: 0, uc: 1, at: 0 }];
|
|
156
|
+
this.pictures = (ctx.run.options.filePlugins?.length ?? 0) > 0 || (ctx.run.verify?.size ?? 0) > 0;
|
|
157
|
+
// A field a strip finding names loses its instructions and keeps its result.
|
|
158
|
+
this.fields = new fields_1.FieldStack(ctx, 'document', marks => {
|
|
159
|
+
for (const [start, end] of this.codes.get(marks.begin) ?? [])
|
|
160
|
+
if (end >= 0)
|
|
161
|
+
cuts.push({ start, end });
|
|
162
|
+
});
|
|
138
163
|
}
|
|
139
164
|
get top() {
|
|
140
165
|
return this.frames[this.frames.length - 1];
|
|
@@ -152,6 +177,11 @@ class RtfReader {
|
|
|
152
177
|
}
|
|
153
178
|
else if (top.kind === 'objskip')
|
|
154
179
|
top.data.hex.binary(b.subarray(i, i + n));
|
|
180
|
+
else if (top.kind === 'pict') {
|
|
181
|
+
if (top.picture.dataAt < 0)
|
|
182
|
+
top.picture.dataAt = this.base + i;
|
|
183
|
+
top.hex.binary(b.subarray(i, i + n));
|
|
184
|
+
}
|
|
155
185
|
this.note(b, i, i + n);
|
|
156
186
|
this.binLeft -= n;
|
|
157
187
|
i += n;
|
|
@@ -160,12 +190,12 @@ class RtfReader {
|
|
|
160
190
|
const c = b[i];
|
|
161
191
|
if (c === 0x7b) {
|
|
162
192
|
this.note(b, i, i + 1);
|
|
163
|
-
this.open();
|
|
193
|
+
this.open(this.base + i);
|
|
164
194
|
i++;
|
|
165
195
|
}
|
|
166
196
|
else if (c === 0x7d) {
|
|
167
197
|
this.note(b, i, i + 1);
|
|
168
|
-
this.close(this.base + i);
|
|
198
|
+
this.close(this.base + i + 1);
|
|
169
199
|
i++;
|
|
170
200
|
}
|
|
171
201
|
else if (c === 0x5c) {
|
|
@@ -190,7 +220,9 @@ class RtfReader {
|
|
|
190
220
|
}
|
|
191
221
|
async objects() {
|
|
192
222
|
for (const o of this.pending.splice(0))
|
|
193
|
-
await objectRules(this.ctx, o);
|
|
223
|
+
await objectRules(this.ctx, o, this.cuts);
|
|
224
|
+
for (const p of this.pendingPictures.splice(0))
|
|
225
|
+
await pictureRules(this.ctx, p, this.cuts);
|
|
194
226
|
}
|
|
195
227
|
async end() {
|
|
196
228
|
// A file can end inside groups. Word reads what it has, and so does this.
|
|
@@ -198,8 +230,8 @@ class RtfReader {
|
|
|
198
230
|
this.close(this.size);
|
|
199
231
|
this.fields.finish();
|
|
200
232
|
await this.objects();
|
|
201
|
-
if (this.trailing > 0)
|
|
202
|
-
this.
|
|
233
|
+
if (this.trailing > 0 && this.ctx.report(types_1.OfficeCategory.Container, types_1.OfficeDetail.TrailingBytes, undefined, { bytes: this.trailing }).action === 'strip')
|
|
234
|
+
this.cuts.push({ start: this.closedAt, end: this.size });
|
|
203
235
|
}
|
|
204
236
|
/** Counts bytes past the document's outermost group that are not white space or NUL. */
|
|
205
237
|
note(b, from, to) {
|
|
@@ -209,23 +241,26 @@ class RtfReader {
|
|
|
209
241
|
if (b[k] > 0x20)
|
|
210
242
|
this.trailing++;
|
|
211
243
|
}
|
|
212
|
-
open() {
|
|
244
|
+
open(offset) {
|
|
245
|
+
this.groups.push(offset);
|
|
213
246
|
this.depth++;
|
|
214
247
|
this.skipChars = 0;
|
|
215
248
|
const limit = this.ctx.settings.rtfDepth;
|
|
216
249
|
if (this.depth > limit)
|
|
217
250
|
throw new errors_1.StopError(types_1.OfficeCategory.Limit, types_1.OfficeDetail.RtfDepth, `Groups nest deeper than ${limit}`, { limit });
|
|
218
251
|
}
|
|
219
|
-
|
|
252
|
+
/** Closes the group open now. `end` is the offset after its brace, or the end of the file. */
|
|
253
|
+
close(end) {
|
|
220
254
|
this.skipChars = 0;
|
|
221
255
|
this.starred = false;
|
|
222
256
|
while (this.frames.length > 1 && this.top.depth >= this.depth)
|
|
223
|
-
this.finishFrame(this.frames.pop());
|
|
257
|
+
this.finishFrame(this.frames.pop(), end);
|
|
224
258
|
if (this.depth === 0)
|
|
225
259
|
return;
|
|
226
260
|
this.depth--;
|
|
261
|
+
this.groups.pop();
|
|
227
262
|
if (this.depth === 0 && this.closedAt < 0)
|
|
228
|
-
this.closedAt =
|
|
263
|
+
this.closedAt = end;
|
|
229
264
|
}
|
|
230
265
|
/**
|
|
231
266
|
* Opens a destination for the rest of the group. One opened in the same group as the last ends that one, as in
|
|
@@ -234,11 +269,11 @@ class RtfReader {
|
|
|
234
269
|
push(body) {
|
|
235
270
|
const top = this.top;
|
|
236
271
|
if (this.frames.length > 1 && top.depth === this.depth && top.kind !== 'field' && top.kind !== 'object')
|
|
237
|
-
this.finishFrame(this.frames.pop());
|
|
272
|
+
this.finishFrame(this.frames.pop(), -1);
|
|
238
273
|
const limit = this.ctx.settings.rtfDepth;
|
|
239
274
|
if (this.frames.length > limit)
|
|
240
275
|
throw new errors_1.StopError(types_1.OfficeCategory.Limit, types_1.OfficeDetail.RtfDepth, `More than ${limit} destinations are open at once`, { limit });
|
|
241
|
-
this.frames.push({ ...body, depth: this.depth, uc: this.top.uc });
|
|
276
|
+
this.frames.push({ ...body, depth: this.depth, uc: this.top.uc, at: this.groups[this.depth - 1] ?? 0 });
|
|
242
277
|
}
|
|
243
278
|
/** Reads a control word or symbol at `i`. Returns the bytes it takes, or 0 when the buffer ends inside it. */
|
|
244
279
|
control(b, i, last) {
|
|
@@ -277,6 +312,7 @@ class RtfReader {
|
|
|
277
312
|
const param = digits === '' ? undefined : Number(digits);
|
|
278
313
|
// A space ends the control word and belongs to it.
|
|
279
314
|
const used = k - i + (k < b.length && b[k] === 0x20 ? 1 : 0);
|
|
315
|
+
this.wordEnd = this.base + i + used;
|
|
280
316
|
this.word(name, param, this.base + i);
|
|
281
317
|
return used;
|
|
282
318
|
}
|
|
@@ -287,6 +323,10 @@ class RtfReader {
|
|
|
287
323
|
if (name === 'bin') {
|
|
288
324
|
// Word reads a negative or missing length as none.
|
|
289
325
|
this.binLeft = param !== undefined && param > 0 ? Math.min(param, Number.MAX_SAFE_INTEGER) : 0;
|
|
326
|
+
// A picture's data that starts with \bin is replaced from the control word on, so no length is left behind.
|
|
327
|
+
const pict = this.top;
|
|
328
|
+
if (pict.kind === 'pict' && pict.picture.dataAt < 0)
|
|
329
|
+
pict.picture.dataAt = offset;
|
|
290
330
|
return;
|
|
291
331
|
}
|
|
292
332
|
const top = this.top;
|
|
@@ -304,23 +344,44 @@ class RtfReader {
|
|
|
304
344
|
return;
|
|
305
345
|
}
|
|
306
346
|
const object = this.object();
|
|
307
|
-
if (
|
|
347
|
+
if (name === 'pict' && this.pictures) {
|
|
348
|
+
const limit = this.ctx.settings.decompressedBytes;
|
|
349
|
+
this.push({ kind: 'pict', picture: { offset, dataAt: -1, group: [0, -1] }, hex: new HexData(offset, limit) });
|
|
350
|
+
const opened = this.top;
|
|
351
|
+
if (opened.kind === 'pict')
|
|
352
|
+
opened.picture.group[0] = opened.at;
|
|
353
|
+
}
|
|
354
|
+
else if (top.kind === 'pict' && Object.hasOwn(PICTURE_TYPES, name))
|
|
355
|
+
top.picture.type = PICTURE_TYPES[name];
|
|
356
|
+
else if (INERT.has(name))
|
|
308
357
|
this.push({ kind: 'skip' });
|
|
309
358
|
else if (name === 'field') {
|
|
310
|
-
this.
|
|
311
|
-
this.
|
|
359
|
+
const field = this.fieldCount++;
|
|
360
|
+
this.fields.begin(field);
|
|
361
|
+
this.push({ kind: 'field', field });
|
|
312
362
|
}
|
|
313
363
|
else if (name === 'fldinst')
|
|
314
|
-
this.push({ kind: 'fldinst' });
|
|
364
|
+
this.push({ kind: 'fldinst', field: this.field() });
|
|
315
365
|
else if (name === 'fldrslt') {
|
|
316
366
|
this.fields.separate();
|
|
317
367
|
this.push({ kind: 'fldrslt' });
|
|
318
368
|
}
|
|
319
|
-
else if (name === 'object')
|
|
369
|
+
else if (name === 'object') {
|
|
320
370
|
this.push({ kind: 'object', object: { offset, kinds: new Set(), update: false, className: '' } });
|
|
371
|
+
const opened = this.top;
|
|
372
|
+
if (opened.kind === 'object')
|
|
373
|
+
opened.object.group = [opened.at, -1];
|
|
374
|
+
}
|
|
321
375
|
else if (name === 'objupdate') {
|
|
322
|
-
if (object)
|
|
376
|
+
if (object) {
|
|
323
377
|
object.update = true;
|
|
378
|
+
object.updateAt ??= [offset, this.wordEnd];
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
else if (name === 'result' && object && !object.result) {
|
|
382
|
+
// What Word shows in place of the object, which stays when the object goes.
|
|
383
|
+
object.result = [this.wordEnd, -1];
|
|
384
|
+
this.push({ kind: 'result', object });
|
|
324
385
|
}
|
|
325
386
|
else if (OBJECT_KINDS.has(name))
|
|
326
387
|
object?.kinds.add(name);
|
|
@@ -350,6 +411,15 @@ class RtfReader {
|
|
|
350
411
|
else if (starred)
|
|
351
412
|
this.push({ kind: 'skip' });
|
|
352
413
|
}
|
|
414
|
+
/** The number of the innermost field the reader is in, or -1. */
|
|
415
|
+
field() {
|
|
416
|
+
for (let k = this.frames.length - 1; k > 0; k--) {
|
|
417
|
+
const f = this.frames[k];
|
|
418
|
+
if (f.kind === 'field')
|
|
419
|
+
return f.field;
|
|
420
|
+
}
|
|
421
|
+
return -1;
|
|
422
|
+
}
|
|
353
423
|
/** The object of the innermost object group the reader is in. */
|
|
354
424
|
object() {
|
|
355
425
|
for (let k = this.frames.length - 1; k > 0; k--) {
|
|
@@ -373,6 +443,10 @@ class RtfReader {
|
|
|
373
443
|
top.data.hex.quote();
|
|
374
444
|
return;
|
|
375
445
|
}
|
|
446
|
+
if (top.kind === 'pict') {
|
|
447
|
+
top.hex.quote();
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
376
450
|
// A file can end before the two characters of a \'hh.
|
|
377
451
|
const hi = hexValue(b[at] ?? -1);
|
|
378
452
|
const lo = hexValue(b[at + 1] ?? -1);
|
|
@@ -384,6 +458,17 @@ class RtfReader {
|
|
|
384
458
|
if (top.kind === 'skip')
|
|
385
459
|
return;
|
|
386
460
|
this.starred = false;
|
|
461
|
+
if (top.kind === 'pict') {
|
|
462
|
+
for (let k = from; k < to; k++) {
|
|
463
|
+
const v = hexValue(b[k]);
|
|
464
|
+
if (v < 0)
|
|
465
|
+
continue;
|
|
466
|
+
if (top.picture.dataAt < 0)
|
|
467
|
+
top.picture.dataAt = this.base + k;
|
|
468
|
+
top.hex.digit(v);
|
|
469
|
+
}
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
387
472
|
if (top.kind === 'objdata' || top.kind === 'objskip') {
|
|
388
473
|
const both = top.kind === 'objdata';
|
|
389
474
|
const hex = both ? top.hex : top.data.hex;
|
|
@@ -421,12 +506,41 @@ class RtfReader {
|
|
|
421
506
|
const top = this.top;
|
|
422
507
|
if (top.kind === 'fldinst')
|
|
423
508
|
this.fields.text(s);
|
|
509
|
+
else if (top.kind === 'fldrslt')
|
|
510
|
+
this.fields.result(s);
|
|
424
511
|
else if ((top.kind === 'objclass' || top.kind === 'template' || top.kind === 'hlsrc') && top.text.length < MAX_TEXT)
|
|
425
512
|
top.text += s;
|
|
426
513
|
}
|
|
427
|
-
|
|
514
|
+
/** Ends a destination. `end` is where its group ends, or -1 when another destination in its group ended it. */
|
|
515
|
+
finishFrame(f, end) {
|
|
516
|
+
// A cut takes a whole group, which only a group that closed has.
|
|
517
|
+
const cut = (r) => {
|
|
518
|
+
if (r.action === 'strip' && end >= 0)
|
|
519
|
+
this.cuts.push({ start: f.at, end });
|
|
520
|
+
};
|
|
428
521
|
if (f.kind === 'field')
|
|
429
522
|
this.fields.end();
|
|
523
|
+
else if (f.kind === 'fldinst') {
|
|
524
|
+
const list = this.codes.get(f.field) ?? [];
|
|
525
|
+
list.push([f.at, end]);
|
|
526
|
+
this.codes.set(f.field, list);
|
|
527
|
+
}
|
|
528
|
+
else if (f.kind === 'result') {
|
|
529
|
+
if (f.object.result)
|
|
530
|
+
f.object.result[1] = end;
|
|
531
|
+
}
|
|
532
|
+
else if (f.kind === 'pict') {
|
|
533
|
+
const data = f.hex.result();
|
|
534
|
+
this.decoded += data.length;
|
|
535
|
+
const total = this.ctx.settings.totalDecompressedBytes;
|
|
536
|
+
if (this.decoded > total)
|
|
537
|
+
throw new errors_1.StopError(types_1.OfficeCategory.Limit, types_1.OfficeDetail.TotalDecompressedSize, `The objects and pictures decode past ${total} bytes`, { limit: total });
|
|
538
|
+
f.picture.group[1] = end;
|
|
539
|
+
if (f.picture.type && data.length) {
|
|
540
|
+
f.picture.data = data;
|
|
541
|
+
this.pendingPictures.push(f.picture);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
430
544
|
else if (f.kind === 'objclass') {
|
|
431
545
|
if (f.object)
|
|
432
546
|
f.object.className = f.text.trim();
|
|
@@ -434,14 +548,15 @@ class RtfReader {
|
|
|
434
548
|
else if (f.kind === 'template') {
|
|
435
549
|
const path = f.text.trim();
|
|
436
550
|
if (path)
|
|
437
|
-
this.ctx.external(types_1.OfficeDetail.Template, path, 'template', { source: 'RTF template' }, 'template');
|
|
551
|
+
cut(this.ctx.external(types_1.OfficeDetail.Template, path, 'template', { source: 'RTF template' }, 'template'));
|
|
438
552
|
}
|
|
439
553
|
else if (f.kind === 'hlsrc') {
|
|
440
554
|
const target = f.text.trim();
|
|
441
555
|
if (target && !(0, links_1.isFragment)(target))
|
|
442
|
-
this.ctx.link(target, 'hyperlink', { source: 'RTF shape hyperlink' });
|
|
556
|
+
cut(this.ctx.link(target, 'hyperlink', { source: 'RTF shape hyperlink' }));
|
|
443
557
|
}
|
|
444
558
|
else if (f.kind === 'objdata') {
|
|
559
|
+
f.object.objdata = [f.at, end];
|
|
445
560
|
f.object.data = f.hex.result();
|
|
446
561
|
const alt = f.alt.result();
|
|
447
562
|
if (!Buffer.from(alt).equals(Buffer.from(f.object.data)))
|
|
@@ -454,28 +569,77 @@ class RtfReader {
|
|
|
454
569
|
if (f.standalone)
|
|
455
570
|
this.pending.push(f.object);
|
|
456
571
|
}
|
|
457
|
-
else if (f.kind === 'object')
|
|
572
|
+
else if (f.kind === 'object') {
|
|
573
|
+
if (f.object.group)
|
|
574
|
+
f.object.group[1] = end;
|
|
458
575
|
this.pending.push(f.object);
|
|
576
|
+
}
|
|
459
577
|
}
|
|
460
578
|
}
|
|
461
|
-
/**
|
|
462
|
-
|
|
579
|
+
/**
|
|
580
|
+
* One embedded or linked object: what its OLE 1.0 header names, and what its data holds. When anything read inside it
|
|
581
|
+
* makes a strip finding, the object's group goes and its result group, the picture Word shows, takes its place.
|
|
582
|
+
* \objupdate, which ACTION/OBJECT_UPDATE names, goes on its own.
|
|
583
|
+
*/
|
|
584
|
+
async function objectRules(ctx, o, cuts) {
|
|
463
585
|
const location = `object at offset ${o.offset}`;
|
|
464
586
|
const kinds = [...o.kinds];
|
|
465
587
|
const base = { object: location, ...(kinds.length ? { kind: kinds.join(' ') } : {}) };
|
|
466
|
-
if (o.update)
|
|
467
|
-
|
|
468
|
-
|
|
588
|
+
if (o.update && ctx.report(types_1.OfficeCategory.Action, types_1.OfficeDetail.ObjectUpdate, location, base).action === 'strip' && o.updateAt)
|
|
589
|
+
cuts.push({ start: o.updateAt[0], end: o.updateAt[1] });
|
|
590
|
+
const before = ctx.strips;
|
|
591
|
+
if (o.kinds.has('objocx'))
|
|
469
592
|
ctx.report(types_1.OfficeCategory.Control, types_1.OfficeDetail.ActiveX, location, { ...base, ...(o.className ? { className: (0, context_1.clip)(o.className) } : {}) });
|
|
593
|
+
else {
|
|
594
|
+
await objectData(ctx, o, o.data, location, base, cuts);
|
|
595
|
+
// The data read without the groups \* marks, where that reads differently, is the object a reader that skips them sees.
|
|
596
|
+
if (o.alt)
|
|
597
|
+
await objectData(ctx, o, o.alt, location, { ...base, reading: 'without the groups \\* marks' });
|
|
598
|
+
}
|
|
599
|
+
if (ctx.strips === before)
|
|
600
|
+
return;
|
|
601
|
+
const [start, end] = o.group ?? o.objdata ?? [-1, -1];
|
|
602
|
+
if (end < 0)
|
|
603
|
+
return;
|
|
604
|
+
const result = o.group && o.result && o.result[1] >= 0 ? o.result : undefined;
|
|
605
|
+
cuts.push(result ? { start, end, text: Buffer.from('{', 'latin1'), copy: result } : { start, end });
|
|
606
|
+
}
|
|
607
|
+
/** Bytes as the hex digits of an objdata group, in lines of 128 digits as Word writes them. */
|
|
608
|
+
function objdataGroup(bytes) {
|
|
609
|
+
const hex = Buffer.from(bytes).toString('hex');
|
|
610
|
+
const lines = [];
|
|
611
|
+
for (let i = 0; i < hex.length; i += 128)
|
|
612
|
+
lines.push(hex.slice(i, i + 128));
|
|
613
|
+
return Buffer.from(`{\\*\\objdata\r\n${lines.join('\r\n')}}`, 'latin1');
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* A PNG, JPEG or EMF picture goes to the plugins, as the file it is. A plugin that removes it removes its \pict group,
|
|
617
|
+
* and bytes a plugin wrote take the place of its data. A picture no plugin takes stays, since no rule names it.
|
|
618
|
+
*/
|
|
619
|
+
async function pictureRules(ctx, p, cuts) {
|
|
620
|
+
const data = p.data ?? new Uint8Array(0);
|
|
621
|
+
const decision = await (0, contained_1.decideContained)(ctx, { declaredType: p.type, location: `picture at offset ${p.offset}`, chunks: () => (0, io_1.chunksOf)((0, io_1.bufferSource)(data), data.length) });
|
|
622
|
+
const [start, end] = p.group;
|
|
623
|
+
if (!decision || end < 0)
|
|
470
624
|
return;
|
|
625
|
+
if (!decision.keep)
|
|
626
|
+
cuts.push({ start, end });
|
|
627
|
+
else if (decision.replacement && p.dataAt >= 0) {
|
|
628
|
+
const hex = Buffer.from(await (0, io_1.readAll)(decision.replacement)).toString('hex');
|
|
629
|
+
const lines = [];
|
|
630
|
+
for (let i = 0; i < hex.length; i += 128)
|
|
631
|
+
lines.push(hex.slice(i, i + 128));
|
|
632
|
+
// The data runs to the brace that closes the group, which stays.
|
|
633
|
+
cuts.push({ start: p.dataAt, end: end - 1, text: Buffer.from(lines.join('\r\n'), 'latin1') });
|
|
471
634
|
}
|
|
472
|
-
await objectData(ctx, o, o.data, location, base);
|
|
473
|
-
// The data read without the groups \* marks, where that reads differently, is the object a reader that skips them sees.
|
|
474
|
-
if (o.alt)
|
|
475
|
-
await objectData(ctx, o, o.alt, location, { ...base, reading: 'without the groups \\* marks' });
|
|
476
635
|
}
|
|
477
|
-
/**
|
|
478
|
-
|
|
636
|
+
/**
|
|
637
|
+
* An object's data: its OLE 1.0 header, and the Packager data or storage it holds. With `cuts`, for the first reading
|
|
638
|
+
* of the data, the file it holds goes to the plugins first: the file a Packager object holds, or the native data of
|
|
639
|
+
* any other embedded object. A plugin's verdict decides whether the object stays, and the bytes a plugin wrote take
|
|
640
|
+
* the place of the file in the object's data.
|
|
641
|
+
*/
|
|
642
|
+
async function objectData(ctx, o, bytes, location, base, cuts) {
|
|
479
643
|
const ole1 = bytes?.length ? (0, oleobj_1.parseOle1)(bytes) : undefined;
|
|
480
644
|
const className = (ole1?.className || o.className).replace(/\0+$/, '');
|
|
481
645
|
const data = { ...base, ...(className ? { className: (0, context_1.clip)(className) } : {}) };
|
|
@@ -494,11 +658,20 @@ async function objectData(ctx, o, bytes, location, base) {
|
|
|
494
658
|
if (!ole1)
|
|
495
659
|
data.reason = 'its data is not an OLE 1.0 object';
|
|
496
660
|
const native = ole1?.data;
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
661
|
+
const pkg = native && className.toLowerCase() === 'package' ? (0, oleobj_1.parsePackage)(native) : undefined;
|
|
662
|
+
let decision;
|
|
663
|
+
if (cuts && native && ole1?.dataAt !== undefined && className.toLowerCase() !== 'ole2link') {
|
|
664
|
+
const held = pkg?.embedded ? native.subarray(pkg.dataAt, pkg.dataAt + pkg.size) : native;
|
|
665
|
+
decision = await (0, contained_1.decideContained)(ctx, { name: (pkg?.embedded ? pkg.fileName : className) || undefined, location, chunks: () => (0, io_1.chunksOf)((0, io_1.bufferSource)(held), held.length) });
|
|
666
|
+
if (decision?.replacement && o.objdata && o.objdata[1] >= 0) {
|
|
667
|
+
const replaced = await (0, io_1.readAll)(decision.replacement);
|
|
668
|
+
const newNative = pkg?.embedded ? (0, oleobj_1.replaceSized)(native, pkg.dataAt, pkg.size, replaced) : replaced;
|
|
669
|
+
cuts.push({ start: o.objdata[0], end: o.objdata[1], text: objdataGroup((0, oleobj_1.replaceSized)(bytes, ole1.dataAt, native.length, newNative)) });
|
|
670
|
+
}
|
|
501
671
|
}
|
|
672
|
+
const act = (0, contained_1.presence)(decision);
|
|
673
|
+
if (pkg)
|
|
674
|
+
Object.assign(data, (0, objects_1.packageData)(pkg));
|
|
502
675
|
else if (native && native.length >= 8 && cfb_1.CFB_SIGNATURE.every((v, i) => native[i] === v)) {
|
|
503
676
|
let cfb;
|
|
504
677
|
try {
|
|
@@ -533,7 +706,7 @@ async function objectData(ctx, o, bytes, location, base) {
|
|
|
533
706
|
if (className.toLowerCase() === 'ole2link')
|
|
534
707
|
ctx.report(types_1.OfficeCategory.External, types_1.OfficeDetail.Object, location, data);
|
|
535
708
|
else
|
|
536
|
-
ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.OleObject, location, data);
|
|
709
|
+
ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.OleObject, location, data, act);
|
|
537
710
|
if (equation)
|
|
538
711
|
ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Equation, location, data);
|
|
539
712
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decompresses an Outlook message's PidTagRtfCompressed (MS-OXRTFCP 2.1.3.1). Returns undefined for input the
|
|
3
|
+
* specification calls corrupt: cut short, of an unknown type, with a CRC that does not match, or longer than `limit`.
|
|
4
|
+
*/
|
|
5
|
+
export declare function decompressRtf(b: Uint8Array, limit: number): Uint8Array | undefined;
|
|
6
|
+
/** RTF stored as a PidTagRtfCompressed value of type UNCOMPRESSED, whose CRC is 0 (MS-OXRTFCP 2.1.3.1.1). */
|
|
7
|
+
export declare function storeRtf(rtf: Uint8Array): Uint8Array;
|
package/dist/rtfcomp.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.decompressRtf = decompressRtf;
|
|
4
|
+
exports.storeRtf = storeRtf;
|
|
5
|
+
const crc32_1 = require("./crc32");
|
|
6
|
+
/** The dictionary MS-OXRTFCP 2.1.3.1.2 starts from: 207 bytes of RTF that most bodies begin with. */
|
|
7
|
+
const PRESET = Buffer.from('{\\rtf1\\ansi\\mac\\deff0\\deftab720{\\fonttbl;}{\\f0\\fnil \\froman \\fswiss \\fmodern \\fscript \\fdecor MS Sans SerifSymbolArialTimes New RomanCourier{\\colortbl\\red0\\green0\\blue0\r\n\\par \\pard\\plain\\f0\\fs20\\b\\i\\u\\tab\\tx', 'latin1');
|
|
8
|
+
/** COMPTYPE: LZFu for compressed, MELA for stored. */
|
|
9
|
+
const COMPRESSED = 0x75465a4c;
|
|
10
|
+
const UNCOMPRESSED = 0x414c454d;
|
|
11
|
+
/**
|
|
12
|
+
* The CRC of MS-OXRTFCP 2.1.3.2: CRC-32's table, started at 0 and never inverted. zlib's CRC-32 inverts the value it
|
|
13
|
+
* starts from and the value it ends with, so starting it from all ones and inverting its result gives this one.
|
|
14
|
+
*/
|
|
15
|
+
const rtfCrc = (b) => ~(0, crc32_1.crc32)(b, 0xffffffff) >>> 0;
|
|
16
|
+
/**
|
|
17
|
+
* Decompresses an Outlook message's PidTagRtfCompressed (MS-OXRTFCP 2.1.3.1). Returns undefined for input the
|
|
18
|
+
* specification calls corrupt: cut short, of an unknown type, with a CRC that does not match, or longer than `limit`.
|
|
19
|
+
*/
|
|
20
|
+
function decompressRtf(b, limit) {
|
|
21
|
+
if (b.length < 16)
|
|
22
|
+
return undefined;
|
|
23
|
+
const view = Buffer.from(b.buffer, b.byteOffset, b.byteLength);
|
|
24
|
+
const compSize = view.readUInt32LE(0);
|
|
25
|
+
const rawSize = view.readUInt32LE(4);
|
|
26
|
+
const type = view.readUInt32LE(8);
|
|
27
|
+
const crc = view.readUInt32LE(12);
|
|
28
|
+
if (compSize < 12 || compSize + 4 > b.length || rawSize > limit)
|
|
29
|
+
return undefined;
|
|
30
|
+
const contents = view.subarray(16, 4 + compSize);
|
|
31
|
+
if (type === UNCOMPRESSED)
|
|
32
|
+
return contents.subarray(0, rawSize);
|
|
33
|
+
if (type !== COMPRESSED || rtfCrc(contents) !== crc)
|
|
34
|
+
return undefined;
|
|
35
|
+
const dict = Buffer.alloc(4096);
|
|
36
|
+
PRESET.copy(dict);
|
|
37
|
+
let write = PRESET.length;
|
|
38
|
+
const out = Buffer.alloc(rawSize);
|
|
39
|
+
let n = 0;
|
|
40
|
+
let at = 0;
|
|
41
|
+
while (at < contents.length) {
|
|
42
|
+
const control = contents[at++];
|
|
43
|
+
for (let bit = 0; bit < 8; bit++) {
|
|
44
|
+
if ((control & (1 << bit)) === 0) {
|
|
45
|
+
if (at >= contents.length)
|
|
46
|
+
return undefined;
|
|
47
|
+
const c = contents[at++];
|
|
48
|
+
if (n < rawSize)
|
|
49
|
+
out[n++] = c;
|
|
50
|
+
dict[write] = c;
|
|
51
|
+
write = (write + 1) & 4095;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (at + 2 > contents.length)
|
|
55
|
+
return undefined;
|
|
56
|
+
const ref = (contents[at] << 8) | contents[at + 1];
|
|
57
|
+
at += 2;
|
|
58
|
+
let read = ref >> 4;
|
|
59
|
+
if (read === write)
|
|
60
|
+
return out.subarray(0, n);
|
|
61
|
+
for (let k = (ref & 0x0f) + 2; k > 0; k--) {
|
|
62
|
+
const c = dict[read];
|
|
63
|
+
read = (read + 1) & 4095;
|
|
64
|
+
if (n < rawSize)
|
|
65
|
+
out[n++] = c;
|
|
66
|
+
dict[write] = c;
|
|
67
|
+
write = (write + 1) & 4095;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
/** RTF stored as a PidTagRtfCompressed value of type UNCOMPRESSED, whose CRC is 0 (MS-OXRTFCP 2.1.3.1.1). */
|
|
74
|
+
function storeRtf(rtf) {
|
|
75
|
+
const head = Buffer.alloc(16);
|
|
76
|
+
head.writeUInt32LE(rtf.length + 12, 0);
|
|
77
|
+
head.writeUInt32LE(rtf.length, 4);
|
|
78
|
+
head.writeUInt32LE(UNCOMPRESSED, 8);
|
|
79
|
+
return Buffer.concat([head, rtf]);
|
|
80
|
+
}
|
package/dist/score.js
CHANGED
|
@@ -19,7 +19,7 @@ exports.COMBINATIONS = [
|
|
|
19
19
|
{
|
|
20
20
|
name: 'code with data hidden in the archive',
|
|
21
21
|
weight: 10,
|
|
22
|
-
test: has => runsCode(has) && (has(types_1.OfficeCategory.Container, types_1.OfficeDetail.LeadingBytes) || has(types_1.OfficeCategory.Container, types_1.OfficeDetail.HiddenData) || has(types_1.OfficeCategory.Container, types_1.OfficeDetail.TrailingBytes)),
|
|
22
|
+
test: has => runsCode(has) && (has(types_1.OfficeCategory.Container, types_1.OfficeDetail.LeadingBytes) || has(types_1.OfficeCategory.Container, types_1.OfficeDetail.HiddenData) || has(types_1.OfficeCategory.Container, types_1.OfficeDetail.HiddenEntry) || has(types_1.OfficeCategory.Container, types_1.OfficeDetail.TrailingBytes)),
|
|
23
23
|
},
|
|
24
24
|
];
|
|
25
25
|
/** The caller's weights first, then the built-in table. A kind from another package scores at the weight it carries. */
|
package/dist/sniff.d.ts
ADDED
package/dist/sniff.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sniffType = sniffType;
|
|
4
|
+
/**
|
|
5
|
+
* Identifies a file type from its first bytes, with the MIME types pdf-defuse gives, so plugins of the family see one
|
|
6
|
+
* name for each type. Returns a MIME type or undefined.
|
|
7
|
+
*/
|
|
8
|
+
function sniffType(head) {
|
|
9
|
+
const b = Buffer.from(head.buffer, head.byteOffset, head.byteLength);
|
|
10
|
+
// PDF readers accept the header anywhere in the first 1024 bytes.
|
|
11
|
+
const s = b.subarray(0, 1024).toString('latin1');
|
|
12
|
+
if (s.includes('%PDF-'))
|
|
13
|
+
return 'application/pdf';
|
|
14
|
+
if (b[0] === 0x50 && b[1] === 0x4b && (b[2] === 0x03 || b[2] === 0x05 || b[2] === 0x07))
|
|
15
|
+
return 'application/zip';
|
|
16
|
+
if (b[0] === 0x89 && s.startsWith('\x89PNG'))
|
|
17
|
+
return 'image/png';
|
|
18
|
+
if (b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff)
|
|
19
|
+
return 'image/jpeg';
|
|
20
|
+
if (s.startsWith('GIF87a') || s.startsWith('GIF89a'))
|
|
21
|
+
return 'image/gif';
|
|
22
|
+
if (b[0] === 0xd0 && b[1] === 0xcf && b[2] === 0x11 && b[3] === 0xe0)
|
|
23
|
+
return 'application/x-ole-storage';
|
|
24
|
+
if (s.startsWith('{\\rt'))
|
|
25
|
+
return 'application/rtf';
|
|
26
|
+
if (s.startsWith('MZ'))
|
|
27
|
+
return 'application/x-msdownload';
|
|
28
|
+
if (s.startsWith('\x7fELF'))
|
|
29
|
+
return 'application/x-executable';
|
|
30
|
+
const text = s.replace(/^\xef\xbb\xbf/, '').trimStart();
|
|
31
|
+
if (/^<svg[\s>/]/i.test(text) || (/^<\?xml/i.test(text) && /<svg[\s>/]/i.test(s)))
|
|
32
|
+
return 'image/svg+xml';
|
|
33
|
+
if (/^<\?xml/i.test(text))
|
|
34
|
+
return 'application/xml';
|
|
35
|
+
if (/^<!doctype html|^<html[\s>]/i.test(text))
|
|
36
|
+
return 'text/html';
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|