@patrickdk77/office-defuse 0.1.1 → 0.2.1
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 +211 -43
- 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/detect.d.ts +13 -0
- package/dist/detect.js +83 -0
- package/dist/engine.d.ts +66 -1
- package/dist/engine.js +290 -35
- 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 +555 -0
- package/dist/formats.d.ts +9 -0
- package/dist/formats.js +7 -1
- package/dist/formulas.d.ts +27 -0
- package/dist/formulas.js +157 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +11 -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 +87 -1
- package/dist/odf.js +204 -76
- package/dist/offcrypto.d.ts +24 -1
- package/dist/offcrypto.js +308 -69
- package/dist/ole.d.ts +19 -2
- package/dist/ole.js +98 -58
- package/dist/oleobj.d.ts +11 -0
- package/dist/oleobj.js +20 -3
- package/dist/ooxml.d.ts +108 -2
- package/dist/ooxml.js +392 -109
- package/dist/package.d.ts +14 -0
- package/dist/package.js +23 -7
- package/dist/plan.d.ts +104 -0
- package/dist/plan.js +73 -0
- package/dist/plugins.d.ts +8 -0
- package/dist/plugins.js +35 -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 +216 -42
- 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 -6
- package/dist/types.js +14 -0
- package/dist/vsd.d.ts +13 -0
- package/dist/vsd.js +689 -0
- package/dist/word.d.ts +4 -4
- package/dist/word.js +70 -23
- package/dist/xls.d.ts +10 -4
- package/dist/xls.js +58 -10
- 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 +11 -2
- package/dist/zip.js +67 -6
- package/package.json +4 -1
package/dist/rtf.js
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
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");
|
|
9
|
+
const formats_1 = require("./formats");
|
|
8
10
|
const io_1 = require("./io");
|
|
9
11
|
const links_1 = require("./links");
|
|
10
12
|
const objects_1 = require("./objects");
|
|
@@ -43,6 +45,8 @@ const MAX_WORD = 250;
|
|
|
43
45
|
const CHUNK = 262144;
|
|
44
46
|
/** Collected text other than an object's data is short: a class name, a template path, a link. */
|
|
45
47
|
const MAX_TEXT = 4096;
|
|
48
|
+
/** The pictures RTF holds as whole files of their own, by the control word that names their type. */
|
|
49
|
+
const PICTURE_TYPES = { pngblip: 'image/png', jpegblip: 'image/jpeg', emfblip: 'image/emf' };
|
|
46
50
|
/** An object's data as Word decodes its hex digits, with the quirks rtfobj documents. */
|
|
47
51
|
class HexData {
|
|
48
52
|
limit;
|
|
@@ -99,8 +103,10 @@ const isDigit = (c) => c >= 0x30 && c <= 0x39;
|
|
|
99
103
|
* text after the document's last brace is still read, as rtfobj reads it, and reported as trailing data.
|
|
100
104
|
*/
|
|
101
105
|
async function inspectRtf(ctx, source, size) {
|
|
102
|
-
ctx.format =
|
|
103
|
-
const
|
|
106
|
+
ctx.format = formats_1.RTF_FORMAT;
|
|
107
|
+
const cuts = [];
|
|
108
|
+
ctx.run.output = { kind: 'rtf', size, cuts };
|
|
109
|
+
const reader = new RtfReader(ctx, size, cuts);
|
|
104
110
|
for (let pos = 0; pos < size; pos += CHUNK) {
|
|
105
111
|
(0, errors_1.checkTime)(ctx.settings.deadline);
|
|
106
112
|
const chunk = await source.read(pos, Math.min(CHUNK, size - pos));
|
|
@@ -111,6 +117,7 @@ async function inspectRtf(ctx, source, size) {
|
|
|
111
117
|
class RtfReader {
|
|
112
118
|
ctx;
|
|
113
119
|
size;
|
|
120
|
+
cuts;
|
|
114
121
|
frames;
|
|
115
122
|
fields;
|
|
116
123
|
depth = 0;
|
|
@@ -130,11 +137,30 @@ class RtfReader {
|
|
|
130
137
|
decoded = 0;
|
|
131
138
|
/** Objects whose groups closed, read in order once the chunk is done. */
|
|
132
139
|
pending = [];
|
|
133
|
-
|
|
140
|
+
/** The offsets of the braces of the groups open now. */
|
|
141
|
+
groups = [];
|
|
142
|
+
/** Where the control word being read ends. */
|
|
143
|
+
wordEnd = 0;
|
|
144
|
+
fieldCount = 0;
|
|
145
|
+
/** The fldinst groups of each field, by the field's number, which go when a finding strips the field. */
|
|
146
|
+
codes = new Map();
|
|
147
|
+
/** Pictures go to the plugins only when there are any, or expected verdicts to check, so only then are they decoded. */
|
|
148
|
+
pictures;
|
|
149
|
+
pendingPictures = [];
|
|
150
|
+
constructor(ctx, size,
|
|
151
|
+
/** Where the rewrite cuts, which the rules fill in as their findings strip. */
|
|
152
|
+
cuts) {
|
|
134
153
|
this.ctx = ctx;
|
|
135
154
|
this.size = size;
|
|
136
|
-
this.
|
|
137
|
-
this.
|
|
155
|
+
this.cuts = cuts;
|
|
156
|
+
this.frames = [{ kind: 'document', depth: 0, uc: 1, at: 0 }];
|
|
157
|
+
this.pictures = (ctx.run.options.filePlugins?.length ?? 0) > 0 || (ctx.run.verify?.size ?? 0) > 0;
|
|
158
|
+
// A field a strip finding names loses its instructions and keeps its result.
|
|
159
|
+
this.fields = new fields_1.FieldStack(ctx, 'document', marks => {
|
|
160
|
+
for (const [start, end] of this.codes.get(marks.begin) ?? [])
|
|
161
|
+
if (end >= 0)
|
|
162
|
+
cuts.push({ start, end });
|
|
163
|
+
});
|
|
138
164
|
}
|
|
139
165
|
get top() {
|
|
140
166
|
return this.frames[this.frames.length - 1];
|
|
@@ -152,6 +178,11 @@ class RtfReader {
|
|
|
152
178
|
}
|
|
153
179
|
else if (top.kind === 'objskip')
|
|
154
180
|
top.data.hex.binary(b.subarray(i, i + n));
|
|
181
|
+
else if (top.kind === 'pict') {
|
|
182
|
+
if (top.picture.dataAt < 0)
|
|
183
|
+
top.picture.dataAt = this.base + i;
|
|
184
|
+
top.hex.binary(b.subarray(i, i + n));
|
|
185
|
+
}
|
|
155
186
|
this.note(b, i, i + n);
|
|
156
187
|
this.binLeft -= n;
|
|
157
188
|
i += n;
|
|
@@ -160,12 +191,12 @@ class RtfReader {
|
|
|
160
191
|
const c = b[i];
|
|
161
192
|
if (c === 0x7b) {
|
|
162
193
|
this.note(b, i, i + 1);
|
|
163
|
-
this.open();
|
|
194
|
+
this.open(this.base + i);
|
|
164
195
|
i++;
|
|
165
196
|
}
|
|
166
197
|
else if (c === 0x7d) {
|
|
167
198
|
this.note(b, i, i + 1);
|
|
168
|
-
this.close(this.base + i);
|
|
199
|
+
this.close(this.base + i + 1);
|
|
169
200
|
i++;
|
|
170
201
|
}
|
|
171
202
|
else if (c === 0x5c) {
|
|
@@ -190,7 +221,9 @@ class RtfReader {
|
|
|
190
221
|
}
|
|
191
222
|
async objects() {
|
|
192
223
|
for (const o of this.pending.splice(0))
|
|
193
|
-
await objectRules(this.ctx, o);
|
|
224
|
+
await objectRules(this.ctx, o, this.cuts);
|
|
225
|
+
for (const p of this.pendingPictures.splice(0))
|
|
226
|
+
await pictureRules(this.ctx, p, this.cuts);
|
|
194
227
|
}
|
|
195
228
|
async end() {
|
|
196
229
|
// A file can end inside groups. Word reads what it has, and so does this.
|
|
@@ -198,8 +231,8 @@ class RtfReader {
|
|
|
198
231
|
this.close(this.size);
|
|
199
232
|
this.fields.finish();
|
|
200
233
|
await this.objects();
|
|
201
|
-
if (this.trailing > 0)
|
|
202
|
-
this.
|
|
234
|
+
if (this.trailing > 0 && this.ctx.report(types_1.OfficeCategory.Container, types_1.OfficeDetail.TrailingBytes, undefined, { bytes: this.trailing }).action === 'strip')
|
|
235
|
+
this.cuts.push({ start: this.closedAt, end: this.size });
|
|
203
236
|
}
|
|
204
237
|
/** Counts bytes past the document's outermost group that are not white space or NUL. */
|
|
205
238
|
note(b, from, to) {
|
|
@@ -209,23 +242,26 @@ class RtfReader {
|
|
|
209
242
|
if (b[k] > 0x20)
|
|
210
243
|
this.trailing++;
|
|
211
244
|
}
|
|
212
|
-
open() {
|
|
245
|
+
open(offset) {
|
|
246
|
+
this.groups.push(offset);
|
|
213
247
|
this.depth++;
|
|
214
248
|
this.skipChars = 0;
|
|
215
249
|
const limit = this.ctx.settings.rtfDepth;
|
|
216
250
|
if (this.depth > limit)
|
|
217
251
|
throw new errors_1.StopError(types_1.OfficeCategory.Limit, types_1.OfficeDetail.RtfDepth, `Groups nest deeper than ${limit}`, { limit });
|
|
218
252
|
}
|
|
219
|
-
|
|
253
|
+
/** Closes the group open now. `end` is the offset after its brace, or the end of the file. */
|
|
254
|
+
close(end) {
|
|
220
255
|
this.skipChars = 0;
|
|
221
256
|
this.starred = false;
|
|
222
257
|
while (this.frames.length > 1 && this.top.depth >= this.depth)
|
|
223
|
-
this.finishFrame(this.frames.pop());
|
|
258
|
+
this.finishFrame(this.frames.pop(), end);
|
|
224
259
|
if (this.depth === 0)
|
|
225
260
|
return;
|
|
226
261
|
this.depth--;
|
|
262
|
+
this.groups.pop();
|
|
227
263
|
if (this.depth === 0 && this.closedAt < 0)
|
|
228
|
-
this.closedAt =
|
|
264
|
+
this.closedAt = end;
|
|
229
265
|
}
|
|
230
266
|
/**
|
|
231
267
|
* 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 +270,11 @@ class RtfReader {
|
|
|
234
270
|
push(body) {
|
|
235
271
|
const top = this.top;
|
|
236
272
|
if (this.frames.length > 1 && top.depth === this.depth && top.kind !== 'field' && top.kind !== 'object')
|
|
237
|
-
this.finishFrame(this.frames.pop());
|
|
273
|
+
this.finishFrame(this.frames.pop(), -1);
|
|
238
274
|
const limit = this.ctx.settings.rtfDepth;
|
|
239
275
|
if (this.frames.length > limit)
|
|
240
276
|
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 });
|
|
277
|
+
this.frames.push({ ...body, depth: this.depth, uc: this.top.uc, at: this.groups[this.depth - 1] ?? 0 });
|
|
242
278
|
}
|
|
243
279
|
/** Reads a control word or symbol at `i`. Returns the bytes it takes, or 0 when the buffer ends inside it. */
|
|
244
280
|
control(b, i, last) {
|
|
@@ -277,6 +313,7 @@ class RtfReader {
|
|
|
277
313
|
const param = digits === '' ? undefined : Number(digits);
|
|
278
314
|
// A space ends the control word and belongs to it.
|
|
279
315
|
const used = k - i + (k < b.length && b[k] === 0x20 ? 1 : 0);
|
|
316
|
+
this.wordEnd = this.base + i + used;
|
|
280
317
|
this.word(name, param, this.base + i);
|
|
281
318
|
return used;
|
|
282
319
|
}
|
|
@@ -287,6 +324,10 @@ class RtfReader {
|
|
|
287
324
|
if (name === 'bin') {
|
|
288
325
|
// Word reads a negative or missing length as none.
|
|
289
326
|
this.binLeft = param !== undefined && param > 0 ? Math.min(param, Number.MAX_SAFE_INTEGER) : 0;
|
|
327
|
+
// A picture's data that starts with \bin is replaced from the control word on, so no length is left behind.
|
|
328
|
+
const pict = this.top;
|
|
329
|
+
if (pict.kind === 'pict' && pict.picture.dataAt < 0)
|
|
330
|
+
pict.picture.dataAt = offset;
|
|
290
331
|
return;
|
|
291
332
|
}
|
|
292
333
|
const top = this.top;
|
|
@@ -304,23 +345,44 @@ class RtfReader {
|
|
|
304
345
|
return;
|
|
305
346
|
}
|
|
306
347
|
const object = this.object();
|
|
307
|
-
if (
|
|
348
|
+
if (name === 'pict' && this.pictures) {
|
|
349
|
+
const limit = this.ctx.settings.decompressedBytes;
|
|
350
|
+
this.push({ kind: 'pict', picture: { offset, dataAt: -1, group: [0, -1] }, hex: new HexData(offset, limit) });
|
|
351
|
+
const opened = this.top;
|
|
352
|
+
if (opened.kind === 'pict')
|
|
353
|
+
opened.picture.group[0] = opened.at;
|
|
354
|
+
}
|
|
355
|
+
else if (top.kind === 'pict' && Object.hasOwn(PICTURE_TYPES, name))
|
|
356
|
+
top.picture.type = PICTURE_TYPES[name];
|
|
357
|
+
else if (INERT.has(name))
|
|
308
358
|
this.push({ kind: 'skip' });
|
|
309
359
|
else if (name === 'field') {
|
|
310
|
-
this.
|
|
311
|
-
this.
|
|
360
|
+
const field = this.fieldCount++;
|
|
361
|
+
this.fields.begin(field);
|
|
362
|
+
this.push({ kind: 'field', field });
|
|
312
363
|
}
|
|
313
364
|
else if (name === 'fldinst')
|
|
314
|
-
this.push({ kind: 'fldinst' });
|
|
365
|
+
this.push({ kind: 'fldinst', field: this.field() });
|
|
315
366
|
else if (name === 'fldrslt') {
|
|
316
367
|
this.fields.separate();
|
|
317
368
|
this.push({ kind: 'fldrslt' });
|
|
318
369
|
}
|
|
319
|
-
else if (name === 'object')
|
|
370
|
+
else if (name === 'object') {
|
|
320
371
|
this.push({ kind: 'object', object: { offset, kinds: new Set(), update: false, className: '' } });
|
|
372
|
+
const opened = this.top;
|
|
373
|
+
if (opened.kind === 'object')
|
|
374
|
+
opened.object.group = [opened.at, -1];
|
|
375
|
+
}
|
|
321
376
|
else if (name === 'objupdate') {
|
|
322
|
-
if (object)
|
|
377
|
+
if (object) {
|
|
323
378
|
object.update = true;
|
|
379
|
+
object.updateAt ??= [offset, this.wordEnd];
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
else if (name === 'result' && object && !object.result) {
|
|
383
|
+
// What Word shows in place of the object, which stays when the object goes.
|
|
384
|
+
object.result = [this.wordEnd, -1];
|
|
385
|
+
this.push({ kind: 'result', object });
|
|
324
386
|
}
|
|
325
387
|
else if (OBJECT_KINDS.has(name))
|
|
326
388
|
object?.kinds.add(name);
|
|
@@ -350,6 +412,15 @@ class RtfReader {
|
|
|
350
412
|
else if (starred)
|
|
351
413
|
this.push({ kind: 'skip' });
|
|
352
414
|
}
|
|
415
|
+
/** The number of the innermost field the reader is in, or -1. */
|
|
416
|
+
field() {
|
|
417
|
+
for (let k = this.frames.length - 1; k > 0; k--) {
|
|
418
|
+
const f = this.frames[k];
|
|
419
|
+
if (f.kind === 'field')
|
|
420
|
+
return f.field;
|
|
421
|
+
}
|
|
422
|
+
return -1;
|
|
423
|
+
}
|
|
353
424
|
/** The object of the innermost object group the reader is in. */
|
|
354
425
|
object() {
|
|
355
426
|
for (let k = this.frames.length - 1; k > 0; k--) {
|
|
@@ -373,6 +444,10 @@ class RtfReader {
|
|
|
373
444
|
top.data.hex.quote();
|
|
374
445
|
return;
|
|
375
446
|
}
|
|
447
|
+
if (top.kind === 'pict') {
|
|
448
|
+
top.hex.quote();
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
376
451
|
// A file can end before the two characters of a \'hh.
|
|
377
452
|
const hi = hexValue(b[at] ?? -1);
|
|
378
453
|
const lo = hexValue(b[at + 1] ?? -1);
|
|
@@ -384,6 +459,17 @@ class RtfReader {
|
|
|
384
459
|
if (top.kind === 'skip')
|
|
385
460
|
return;
|
|
386
461
|
this.starred = false;
|
|
462
|
+
if (top.kind === 'pict') {
|
|
463
|
+
for (let k = from; k < to; k++) {
|
|
464
|
+
const v = hexValue(b[k]);
|
|
465
|
+
if (v < 0)
|
|
466
|
+
continue;
|
|
467
|
+
if (top.picture.dataAt < 0)
|
|
468
|
+
top.picture.dataAt = this.base + k;
|
|
469
|
+
top.hex.digit(v);
|
|
470
|
+
}
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
387
473
|
if (top.kind === 'objdata' || top.kind === 'objskip') {
|
|
388
474
|
const both = top.kind === 'objdata';
|
|
389
475
|
const hex = both ? top.hex : top.data.hex;
|
|
@@ -421,12 +507,41 @@ class RtfReader {
|
|
|
421
507
|
const top = this.top;
|
|
422
508
|
if (top.kind === 'fldinst')
|
|
423
509
|
this.fields.text(s);
|
|
510
|
+
else if (top.kind === 'fldrslt')
|
|
511
|
+
this.fields.result(s);
|
|
424
512
|
else if ((top.kind === 'objclass' || top.kind === 'template' || top.kind === 'hlsrc') && top.text.length < MAX_TEXT)
|
|
425
513
|
top.text += s;
|
|
426
514
|
}
|
|
427
|
-
|
|
515
|
+
/** Ends a destination. `end` is where its group ends, or -1 when another destination in its group ended it. */
|
|
516
|
+
finishFrame(f, end) {
|
|
517
|
+
// A cut takes a whole group, which only a group that closed has.
|
|
518
|
+
const cut = (r) => {
|
|
519
|
+
if (r.action === 'strip' && end >= 0)
|
|
520
|
+
this.cuts.push({ start: f.at, end });
|
|
521
|
+
};
|
|
428
522
|
if (f.kind === 'field')
|
|
429
523
|
this.fields.end();
|
|
524
|
+
else if (f.kind === 'fldinst') {
|
|
525
|
+
const list = this.codes.get(f.field) ?? [];
|
|
526
|
+
list.push([f.at, end]);
|
|
527
|
+
this.codes.set(f.field, list);
|
|
528
|
+
}
|
|
529
|
+
else if (f.kind === 'result') {
|
|
530
|
+
if (f.object.result)
|
|
531
|
+
f.object.result[1] = end;
|
|
532
|
+
}
|
|
533
|
+
else if (f.kind === 'pict') {
|
|
534
|
+
const data = f.hex.result();
|
|
535
|
+
this.decoded += data.length;
|
|
536
|
+
const total = this.ctx.settings.totalDecompressedBytes;
|
|
537
|
+
if (this.decoded > total)
|
|
538
|
+
throw new errors_1.StopError(types_1.OfficeCategory.Limit, types_1.OfficeDetail.TotalDecompressedSize, `The objects and pictures decode past ${total} bytes`, { limit: total });
|
|
539
|
+
f.picture.group[1] = end;
|
|
540
|
+
if (f.picture.type && data.length) {
|
|
541
|
+
f.picture.data = data;
|
|
542
|
+
this.pendingPictures.push(f.picture);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
430
545
|
else if (f.kind === 'objclass') {
|
|
431
546
|
if (f.object)
|
|
432
547
|
f.object.className = f.text.trim();
|
|
@@ -434,14 +549,15 @@ class RtfReader {
|
|
|
434
549
|
else if (f.kind === 'template') {
|
|
435
550
|
const path = f.text.trim();
|
|
436
551
|
if (path)
|
|
437
|
-
this.ctx.external(types_1.OfficeDetail.Template, path, 'template', { source: 'RTF template' }, 'template');
|
|
552
|
+
cut(this.ctx.external(types_1.OfficeDetail.Template, path, 'template', { source: 'RTF template' }, 'template'));
|
|
438
553
|
}
|
|
439
554
|
else if (f.kind === 'hlsrc') {
|
|
440
555
|
const target = f.text.trim();
|
|
441
556
|
if (target && !(0, links_1.isFragment)(target))
|
|
442
|
-
this.ctx.link(target, 'hyperlink', { source: 'RTF shape hyperlink' });
|
|
557
|
+
cut(this.ctx.link(target, 'hyperlink', { source: 'RTF shape hyperlink' }));
|
|
443
558
|
}
|
|
444
559
|
else if (f.kind === 'objdata') {
|
|
560
|
+
f.object.objdata = [f.at, end];
|
|
445
561
|
f.object.data = f.hex.result();
|
|
446
562
|
const alt = f.alt.result();
|
|
447
563
|
if (!Buffer.from(alt).equals(Buffer.from(f.object.data)))
|
|
@@ -454,28 +570,77 @@ class RtfReader {
|
|
|
454
570
|
if (f.standalone)
|
|
455
571
|
this.pending.push(f.object);
|
|
456
572
|
}
|
|
457
|
-
else if (f.kind === 'object')
|
|
573
|
+
else if (f.kind === 'object') {
|
|
574
|
+
if (f.object.group)
|
|
575
|
+
f.object.group[1] = end;
|
|
458
576
|
this.pending.push(f.object);
|
|
577
|
+
}
|
|
459
578
|
}
|
|
460
579
|
}
|
|
461
|
-
/**
|
|
462
|
-
|
|
580
|
+
/**
|
|
581
|
+
* One embedded or linked object: what its OLE 1.0 header names, and what its data holds. When anything read inside it
|
|
582
|
+
* makes a strip finding, the object's group goes and its result group, the picture Word shows, takes its place.
|
|
583
|
+
* \objupdate, which ACTION/OBJECT_UPDATE names, goes on its own.
|
|
584
|
+
*/
|
|
585
|
+
async function objectRules(ctx, o, cuts) {
|
|
463
586
|
const location = `object at offset ${o.offset}`;
|
|
464
587
|
const kinds = [...o.kinds];
|
|
465
588
|
const base = { object: location, ...(kinds.length ? { kind: kinds.join(' ') } : {}) };
|
|
466
|
-
if (o.update)
|
|
467
|
-
|
|
468
|
-
|
|
589
|
+
if (o.update && ctx.report(types_1.OfficeCategory.Action, types_1.OfficeDetail.ObjectUpdate, location, base).action === 'strip' && o.updateAt)
|
|
590
|
+
cuts.push({ start: o.updateAt[0], end: o.updateAt[1] });
|
|
591
|
+
const before = ctx.strips;
|
|
592
|
+
if (o.kinds.has('objocx'))
|
|
469
593
|
ctx.report(types_1.OfficeCategory.Control, types_1.OfficeDetail.ActiveX, location, { ...base, ...(o.className ? { className: (0, context_1.clip)(o.className) } : {}) });
|
|
594
|
+
else {
|
|
595
|
+
await objectData(ctx, o, o.data, location, base, cuts);
|
|
596
|
+
// The data read without the groups \* marks, where that reads differently, is the object a reader that skips them sees.
|
|
597
|
+
if (o.alt)
|
|
598
|
+
await objectData(ctx, o, o.alt, location, { ...base, reading: 'without the groups \\* marks' });
|
|
599
|
+
}
|
|
600
|
+
if (ctx.strips === before)
|
|
601
|
+
return;
|
|
602
|
+
const [start, end] = o.group ?? o.objdata ?? [-1, -1];
|
|
603
|
+
if (end < 0)
|
|
604
|
+
return;
|
|
605
|
+
const result = o.group && o.result && o.result[1] >= 0 ? o.result : undefined;
|
|
606
|
+
cuts.push(result ? { start, end, text: Buffer.from('{', 'latin1'), copy: result } : { start, end });
|
|
607
|
+
}
|
|
608
|
+
/** Bytes as the hex digits of an objdata group, in lines of 128 digits as Word writes them. */
|
|
609
|
+
function objdataGroup(bytes) {
|
|
610
|
+
const hex = Buffer.from(bytes).toString('hex');
|
|
611
|
+
const lines = [];
|
|
612
|
+
for (let i = 0; i < hex.length; i += 128)
|
|
613
|
+
lines.push(hex.slice(i, i + 128));
|
|
614
|
+
return Buffer.from(`{\\*\\objdata\r\n${lines.join('\r\n')}}`, 'latin1');
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* A PNG, JPEG or EMF picture goes to the plugins, as the file it is. A plugin that removes it removes its \pict group,
|
|
618
|
+
* and bytes a plugin wrote take the place of its data. A picture no plugin takes stays, since no rule names it.
|
|
619
|
+
*/
|
|
620
|
+
async function pictureRules(ctx, p, cuts) {
|
|
621
|
+
const data = p.data ?? new Uint8Array(0);
|
|
622
|
+
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) });
|
|
623
|
+
const [start, end] = p.group;
|
|
624
|
+
if (!decision || end < 0)
|
|
470
625
|
return;
|
|
626
|
+
if (!decision.keep)
|
|
627
|
+
cuts.push({ start, end });
|
|
628
|
+
else if (decision.replacement && p.dataAt >= 0) {
|
|
629
|
+
const hex = Buffer.from(await (0, io_1.readAll)(decision.replacement)).toString('hex');
|
|
630
|
+
const lines = [];
|
|
631
|
+
for (let i = 0; i < hex.length; i += 128)
|
|
632
|
+
lines.push(hex.slice(i, i + 128));
|
|
633
|
+
// The data runs to the brace that closes the group, which stays.
|
|
634
|
+
cuts.push({ start: p.dataAt, end: end - 1, text: Buffer.from(lines.join('\r\n'), 'latin1') });
|
|
471
635
|
}
|
|
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
636
|
}
|
|
477
|
-
/**
|
|
478
|
-
|
|
637
|
+
/**
|
|
638
|
+
* An object's data: its OLE 1.0 header, and the Packager data or storage it holds. With `cuts`, for the first reading
|
|
639
|
+
* of the data, the file it holds goes to the plugins first: the file a Packager object holds, or the native data of
|
|
640
|
+
* any other embedded object. A plugin's verdict decides whether the object stays, and the bytes a plugin wrote take
|
|
641
|
+
* the place of the file in the object's data.
|
|
642
|
+
*/
|
|
643
|
+
async function objectData(ctx, o, bytes, location, base, cuts) {
|
|
479
644
|
const ole1 = bytes?.length ? (0, oleobj_1.parseOle1)(bytes) : undefined;
|
|
480
645
|
const className = (ole1?.className || o.className).replace(/\0+$/, '');
|
|
481
646
|
const data = { ...base, ...(className ? { className: (0, context_1.clip)(className) } : {}) };
|
|
@@ -494,11 +659,20 @@ async function objectData(ctx, o, bytes, location, base) {
|
|
|
494
659
|
if (!ole1)
|
|
495
660
|
data.reason = 'its data is not an OLE 1.0 object';
|
|
496
661
|
const native = ole1?.data;
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
662
|
+
const pkg = native && className.toLowerCase() === 'package' ? (0, oleobj_1.parsePackage)(native) : undefined;
|
|
663
|
+
let decision;
|
|
664
|
+
if (cuts && native && ole1?.dataAt !== undefined && className.toLowerCase() !== 'ole2link') {
|
|
665
|
+
const held = pkg?.embedded ? native.subarray(pkg.dataAt, pkg.dataAt + pkg.size) : native;
|
|
666
|
+
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) });
|
|
667
|
+
if (decision?.replacement && o.objdata && o.objdata[1] >= 0) {
|
|
668
|
+
const replaced = await (0, io_1.readAll)(decision.replacement);
|
|
669
|
+
const newNative = pkg?.embedded ? (0, oleobj_1.replaceSized)(native, pkg.dataAt, pkg.size, replaced) : replaced;
|
|
670
|
+
cuts.push({ start: o.objdata[0], end: o.objdata[1], text: objdataGroup((0, oleobj_1.replaceSized)(bytes, ole1.dataAt, native.length, newNative)) });
|
|
671
|
+
}
|
|
501
672
|
}
|
|
673
|
+
const act = (0, contained_1.presence)(decision);
|
|
674
|
+
if (pkg)
|
|
675
|
+
Object.assign(data, (0, objects_1.packageData)(pkg));
|
|
502
676
|
else if (native && native.length >= 8 && cfb_1.CFB_SIGNATURE.every((v, i) => native[i] === v)) {
|
|
503
677
|
let cfb;
|
|
504
678
|
try {
|
|
@@ -533,7 +707,7 @@ async function objectData(ctx, o, bytes, location, base) {
|
|
|
533
707
|
if (className.toLowerCase() === 'ole2link')
|
|
534
708
|
ctx.report(types_1.OfficeCategory.External, types_1.OfficeDetail.Object, location, data);
|
|
535
709
|
else
|
|
536
|
-
ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.OleObject, location, data);
|
|
710
|
+
ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.OleObject, location, data, act);
|
|
537
711
|
if (equation)
|
|
538
712
|
ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Equation, location, data);
|
|
539
713
|
}
|
|
@@ -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
|
+
}
|