@patrickdk77/office-defuse 0.1.0 → 0.1.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/dist/rtf.js ADDED
@@ -0,0 +1,539 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.inspectRtf = inspectRtf;
4
+ const cfb_1 = require("./cfb");
5
+ const context_1 = require("./context");
6
+ const errors_1 = require("./errors");
7
+ const fields_1 = require("./fields");
8
+ const io_1 = require("./io");
9
+ const links_1 = require("./links");
10
+ const objects_1 = require("./objects");
11
+ const oleobj_1 = require("./oleobj");
12
+ const types_1 = require("./types");
13
+ /**
14
+ * The destinations of the RTF 1.9.1 specification, which rtfobj lists too. Word sends the text of each to its own
15
+ * place, so none of it is object data or a field code. \rtf is not one, though it opens the document.
16
+ */
17
+ const DESTINATIONS = new Set(('aftncn aftnsep aftnsepc annotation atnauthor atndate atnid atnparent atnref atrfend atrfstart author background bkmkend bkmkstart ' +
18
+ 'blipuid buptim category colorschememapping colortbl comment company creatim datafield datastore defchp defpap do doccomm docvar ' +
19
+ 'dptxbxtext ebcend ebcstart factoidname falt fchars ffdeftext ffentrymcr ffexitmcr ffformat ffhelptext ffl ffname ffstattext field ' +
20
+ 'file filetbl fldinst fldrslt fldtype fontemb fonttbl footer footerf footerl footerr footnote formfield ftncn ftnsep ftnsepc g ' +
21
+ 'generator gridtbl header headerf headerl headerr hl hlfr hlinkbase hlloc hlsrc hsv info keywords latentstyles lchars levelnumbers ' +
22
+ 'leveltext lfolevel linkval list listlevel listname listoverride listoverridetable listpicture liststylename listtable listtext ' +
23
+ 'lsdlockedexcept macc maccPr mailmerge malnScr manager margPr mbar mbarPr mbaseJc mbegChr mborderBox mborderBoxPr mbox mboxPr mchr ' +
24
+ 'mcount mctrlPr md mdeg mdegHide mden mdiff mdPr me mendChr meqArr meqArrPr mf mfName mfPr mfunc mfuncPr mgroupChr mgroupChrPr mgrow ' +
25
+ 'mhideBot mhideLeft mhideRight mhideTop mlim mlimLoc mlimLow mlimLowPr mlimUpp mlimUppPr mm mmaddfieldname mmathPict mmaxDist mmc ' +
26
+ 'mmcJc mmconnectstr mmconnectstrdata mmcPr mmcs mmdatasource mmheadersource mmmailsubject mmodso mmodsofilter mmodsofldmpdata ' +
27
+ 'mmodsomappedname mmodsoname mmodsorecipdata mmodsosort mmodsosrc mmodsotable mmodsoudl mmodsoudldata mmodsouniquetag mmPr mmquery ' +
28
+ 'mmr mnary mnaryPr mnoBreak mnum mobjDist moMath moMathPara moMathParaPr mopEmu mphant mphantPr mplcHide mpos mr mrad mradPr mrPr ' +
29
+ 'msepChr mshow mshp msPre msPrePr msSub msSubPr msSubSup msSubSupPr msSup msSupPr mstrikeBLTR mstrikeH mstrikeTLBR mstrikeV msub ' +
30
+ 'msubHide msup msupHide mtransp mtype mvertJc mvfmf mvfml mvtof mvtol mzeroAsc mzeroDesc mzeroWid nesttableprops nonesttables ' +
31
+ 'objalias objclass objdata object objname objsect oldcprops oldpprops oldsprops oldtprops oleclsid operator panose password ' +
32
+ 'passwordhash pgp pgptbl picprop pict pn pnseclvl pntext pntxta pntxtb printim propname protend protstart protusertbl result revtbl ' +
33
+ 'revtim rxe shp shpgrp shpinst shppict shprslt shptxt sn sp staticval stylesheet subject sv svb tc template themedata title txe ud ' +
34
+ 'upr userprops wgrffmtfilter windowcaption writereservation writereservhash xe xform xmlattrname xmlattrvalue xmlclose xmlname ' +
35
+ 'xmlnstbl xmlopen margSz pnaiu pnaiud').split(' '));
36
+ /** Destinations whose content holds no field, object or link, so their groups are skipped whole: tables, styles, pictures, metadata. */
37
+ const INERT = new Set(('fonttbl colortbl stylesheet info pict themedata colorschememapping datastore latentstyles listtable listoverridetable revtbl ' +
38
+ 'generator xmlnstbl filetbl pgptbl protusertbl blipuid datafield fontemb panose falt password passwordhash wgrffmtfilter').split(' '));
39
+ /** Object kinds, from the control word that sets them. */
40
+ const OBJECT_KINDS = new Set(['objemb', 'objlink', 'objautlink', 'objsub', 'objpub', 'objicemb', 'objhtml', 'objocx']);
41
+ /** Word reads at most this many letters of a control word's name and digits of its parameter. */
42
+ const MAX_WORD = 250;
43
+ const CHUNK = 262144;
44
+ /** Collected text other than an object's data is short: a class name, a template path, a link. */
45
+ const MAX_TEXT = 4096;
46
+ /** An object's data as Word decodes its hex digits, with the quirks rtfobj documents. */
47
+ class HexData {
48
+ limit;
49
+ bytes;
50
+ length = 0;
51
+ /** A hex digit read and not yet paired, or -1. */
52
+ nibble = -1;
53
+ offset;
54
+ constructor(offset, limit) {
55
+ this.limit = limit;
56
+ this.offset = offset;
57
+ this.bytes = new Uint8Array(1024);
58
+ }
59
+ digit(v) {
60
+ if (this.nibble < 0) {
61
+ this.nibble = v;
62
+ return;
63
+ }
64
+ this.push((this.nibble << 4) | v);
65
+ this.nibble = -1;
66
+ }
67
+ /** Word drops a hex digit left unpaired before a \'hh, and so does rtfobj. */
68
+ quote() {
69
+ this.nibble = -1;
70
+ }
71
+ /** \bin data in hex goes in as its bytes, two digits each, so an unpaired digit stays unpaired. */
72
+ binary(b) {
73
+ for (const x of b) {
74
+ this.digit(x >> 4);
75
+ this.digit(x & 15);
76
+ }
77
+ }
78
+ push(b) {
79
+ if (this.length >= this.limit)
80
+ throw new errors_1.StopError(types_1.OfficeCategory.Limit, types_1.OfficeDetail.DecompressedSize, `The data of the object at offset ${this.offset} decodes past ${this.limit} bytes`, { limit: this.limit });
81
+ if (this.length === this.bytes.length) {
82
+ const grown = new Uint8Array(Math.min(this.limit, this.bytes.length * 2));
83
+ grown.set(this.bytes);
84
+ this.bytes = grown;
85
+ }
86
+ this.bytes[this.length++] = b;
87
+ }
88
+ /** The decoded bytes. A last digit left unpaired is dropped, as Word drops it. */
89
+ result() {
90
+ return this.bytes.subarray(0, this.length);
91
+ }
92
+ }
93
+ const hexValue = (c) => (c >= 0x30 && c <= 0x39 ? c - 0x30 : c >= 0x41 && c <= 0x46 ? c - 0x37 : c >= 0x61 && c <= 0x66 ? c - 0x57 : -1);
94
+ const isLetter = (c) => (c >= 0x41 && c <= 0x5a) || (c >= 0x61 && c <= 0x7a);
95
+ const isDigit = (c) => c >= 0x30 && c <= 0x39;
96
+ /**
97
+ * Inspects an RTF file the way Word reads one. Objects with their data, fields, the attached template and shape
98
+ * hyperlinks are reported. Unknown destinations marked with \* are skipped, unknown control words are ignored, and
99
+ * text after the document's last brace is still read, as rtfobj reads it, and reported as trailing data.
100
+ */
101
+ async function inspectRtf(ctx, source, size) {
102
+ ctx.format = { format: 'rtf', mimeType: 'application/rtf' };
103
+ const reader = new RtfReader(ctx, size);
104
+ for (let pos = 0; pos < size; pos += CHUNK) {
105
+ (0, errors_1.checkTime)(ctx.settings.deadline);
106
+ const chunk = await source.read(pos, Math.min(CHUNK, size - pos));
107
+ await reader.write(chunk, pos + chunk.length >= size);
108
+ }
109
+ await reader.end();
110
+ }
111
+ class RtfReader {
112
+ ctx;
113
+ size;
114
+ frames;
115
+ fields;
116
+ depth = 0;
117
+ /** A partial token carried from one chunk to the next. */
118
+ carry = new Uint8Array(0);
119
+ /** The file offset of the first byte of the buffer being read. */
120
+ base = 0;
121
+ /** \bin bytes still to take, and where they go. */
122
+ binLeft = 0;
123
+ /** Fallback characters still to skip after a \u. */
124
+ skipChars = 0;
125
+ /** Set by \*, which makes an unknown destination that follows skipped. */
126
+ starred = false;
127
+ /** Where the document's outermost group closed, and how much that is not white space comes after it. */
128
+ closedAt = -1;
129
+ trailing = 0;
130
+ decoded = 0;
131
+ /** Objects whose groups closed, read in order once the chunk is done. */
132
+ pending = [];
133
+ constructor(ctx, size) {
134
+ this.ctx = ctx;
135
+ this.size = size;
136
+ this.frames = [{ kind: 'document', depth: 0, uc: 1 }];
137
+ this.fields = new fields_1.FieldStack(ctx, 'document');
138
+ }
139
+ get top() {
140
+ return this.frames[this.frames.length - 1];
141
+ }
142
+ async write(chunk, last) {
143
+ const b = this.carry.length ? Buffer.concat([this.carry, chunk]) : chunk;
144
+ let i = 0;
145
+ while (i < b.length) {
146
+ if (this.binLeft > 0) {
147
+ const n = Math.min(this.binLeft, b.length - i);
148
+ const top = this.top;
149
+ if (top.kind === 'objdata') {
150
+ top.hex.binary(b.subarray(i, i + n));
151
+ top.alt.binary(b.subarray(i, i + n));
152
+ }
153
+ else if (top.kind === 'objskip')
154
+ top.data.hex.binary(b.subarray(i, i + n));
155
+ this.note(b, i, i + n);
156
+ this.binLeft -= n;
157
+ i += n;
158
+ continue;
159
+ }
160
+ const c = b[i];
161
+ if (c === 0x7b) {
162
+ this.note(b, i, i + 1);
163
+ this.open();
164
+ i++;
165
+ }
166
+ else if (c === 0x7d) {
167
+ this.note(b, i, i + 1);
168
+ this.close(this.base + i);
169
+ i++;
170
+ }
171
+ else if (c === 0x5c) {
172
+ const used = this.control(b, i, last);
173
+ if (used === 0)
174
+ break;
175
+ this.note(b, i, i + used);
176
+ i += used;
177
+ }
178
+ else {
179
+ let j = i;
180
+ while (j < b.length && b[j] !== 0x7b && b[j] !== 0x7d && b[j] !== 0x5c)
181
+ j++;
182
+ this.note(b, i, j);
183
+ this.text(b, i, j);
184
+ i = j;
185
+ }
186
+ }
187
+ this.carry = i < b.length ? b.slice(i) : new Uint8Array(0);
188
+ this.base += i;
189
+ await this.objects();
190
+ }
191
+ async objects() {
192
+ for (const o of this.pending.splice(0))
193
+ await objectRules(this.ctx, o);
194
+ }
195
+ async end() {
196
+ // A file can end inside groups. Word reads what it has, and so does this.
197
+ while (this.depth > 0)
198
+ this.close(this.size);
199
+ this.fields.finish();
200
+ await this.objects();
201
+ if (this.trailing > 0)
202
+ this.ctx.report(types_1.OfficeCategory.Container, types_1.OfficeDetail.TrailingBytes, undefined, { bytes: this.trailing });
203
+ }
204
+ /** Counts bytes past the document's outermost group that are not white space or NUL. */
205
+ note(b, from, to) {
206
+ if (this.closedAt < 0)
207
+ return;
208
+ for (let k = from; k < to; k++)
209
+ if (b[k] > 0x20)
210
+ this.trailing++;
211
+ }
212
+ open() {
213
+ this.depth++;
214
+ this.skipChars = 0;
215
+ const limit = this.ctx.settings.rtfDepth;
216
+ if (this.depth > limit)
217
+ throw new errors_1.StopError(types_1.OfficeCategory.Limit, types_1.OfficeDetail.RtfDepth, `Groups nest deeper than ${limit}`, { limit });
218
+ }
219
+ close(offset) {
220
+ this.skipChars = 0;
221
+ this.starred = false;
222
+ while (this.frames.length > 1 && this.top.depth >= this.depth)
223
+ this.finishFrame(this.frames.pop());
224
+ if (this.depth === 0)
225
+ return;
226
+ this.depth--;
227
+ if (this.depth === 0 && this.closedAt < 0)
228
+ this.closedAt = offset;
229
+ }
230
+ /**
231
+ * Opens a destination for the rest of the group. One opened in the same group as the last ends that one, as in
232
+ * rtfobj, unless the last is a field or an object, whose group the destination belongs to.
233
+ */
234
+ push(body) {
235
+ const top = this.top;
236
+ if (this.frames.length > 1 && top.depth === this.depth && top.kind !== 'field' && top.kind !== 'object')
237
+ this.finishFrame(this.frames.pop());
238
+ const limit = this.ctx.settings.rtfDepth;
239
+ if (this.frames.length > limit)
240
+ 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 });
242
+ }
243
+ /** Reads a control word or symbol at `i`. Returns the bytes it takes, or 0 when the buffer ends inside it. */
244
+ control(b, i, last) {
245
+ if (i + 1 >= b.length)
246
+ return last ? 1 : 0;
247
+ const c = b[i + 1];
248
+ if (!isLetter(c)) {
249
+ if (c === 0x27) {
250
+ if (i + 3 >= b.length && !last)
251
+ return 0;
252
+ this.quoted(b, i + 2);
253
+ return Math.min(4, b.length - i);
254
+ }
255
+ if (c === 0x2a)
256
+ this.starred = true;
257
+ else if (c === 0x5c || c === 0x7b || c === 0x7d)
258
+ this.char(String.fromCharCode(c));
259
+ else if (c === 0x0a || c === 0x0d)
260
+ this.char('\n');
261
+ else if (c === 0x7e)
262
+ this.char(' ');
263
+ return 2;
264
+ }
265
+ let j = i + 1;
266
+ while (j < b.length && isLetter(b[j]) && j - i - 1 < MAX_WORD)
267
+ j++;
268
+ let k = j;
269
+ if (k < b.length && b[k] === 0x2d && k + 1 < b.length && isDigit(b[k + 1]))
270
+ k++;
271
+ while (k < b.length && isDigit(b[k]) && k - j < MAX_WORD)
272
+ k++;
273
+ if (k >= b.length && !last)
274
+ return 0;
275
+ const name = (0, oleobj_1.latin1)(b.subarray(i + 1, j));
276
+ const digits = (0, oleobj_1.latin1)(b.subarray(j, k));
277
+ const param = digits === '' ? undefined : Number(digits);
278
+ // A space ends the control word and belongs to it.
279
+ const used = k - i + (k < b.length && b[k] === 0x20 ? 1 : 0);
280
+ this.word(name, param, this.base + i);
281
+ return used;
282
+ }
283
+ word(name, param, offset) {
284
+ const starred = this.starred;
285
+ this.starred = false;
286
+ // \bin data is taken whole wherever it is, since its bytes may hold braces.
287
+ if (name === 'bin') {
288
+ // Word reads a negative or missing length as none.
289
+ this.binLeft = param !== undefined && param > 0 ? Math.min(param, Number.MAX_SAFE_INTEGER) : 0;
290
+ return;
291
+ }
292
+ const top = this.top;
293
+ if (top.kind === 'skip')
294
+ return;
295
+ if (name === 'u' && param !== undefined) {
296
+ this.skipChars = 0;
297
+ this.char(String.fromCharCode(param < 0 ? param + 65536 : param & 0xffff));
298
+ this.skipChars = top.uc;
299
+ return;
300
+ }
301
+ this.skipChars = 0;
302
+ if (name === 'uc') {
303
+ top.uc = param ?? 1;
304
+ return;
305
+ }
306
+ const object = this.object();
307
+ if (INERT.has(name))
308
+ this.push({ kind: 'skip' });
309
+ else if (name === 'field') {
310
+ this.fields.begin();
311
+ this.push({ kind: 'field' });
312
+ }
313
+ else if (name === 'fldinst')
314
+ this.push({ kind: 'fldinst' });
315
+ else if (name === 'fldrslt') {
316
+ this.fields.separate();
317
+ this.push({ kind: 'fldrslt' });
318
+ }
319
+ else if (name === 'object')
320
+ this.push({ kind: 'object', object: { offset, kinds: new Set(), update: false, className: '' } });
321
+ else if (name === 'objupdate') {
322
+ if (object)
323
+ object.update = true;
324
+ }
325
+ else if (OBJECT_KINDS.has(name))
326
+ object?.kinds.add(name);
327
+ else if (name === 'objdata') {
328
+ const limit = this.ctx.settings.decompressedBytes;
329
+ this.push({
330
+ kind: 'objdata',
331
+ object: object ?? { offset, kinds: new Set(), update: false, className: '' },
332
+ hex: new HexData(offset, limit),
333
+ alt: new HexData(offset, limit),
334
+ standalone: !object,
335
+ });
336
+ }
337
+ else if (name === 'objclass')
338
+ this.push({ kind: 'objclass', text: '', object });
339
+ else if (name === 'template' || (name === 'hlsrc' && this.frames.some(f => f.kind === 'hl')))
340
+ this.push({ kind: name, text: '' });
341
+ else if (name === 'hl')
342
+ this.push({ kind: 'hl' });
343
+ // Any other destination takes its own text. What it holds is still read for fields and objects.
344
+ else if (DESTINATIONS.has(name))
345
+ this.push({ kind: 'document' });
346
+ // Word skips a group that \* marks when it does not know its control word. Inside object data rtfobj keeps it, and
347
+ // a sample Word opens hides its hex digits in groups such as {\*\par574 0}. Both readings of the data are kept.
348
+ else if (starred && (top.kind === 'objdata' || top.kind === 'objskip'))
349
+ this.push({ kind: 'objskip', data: top.kind === 'objdata' ? top : top.data });
350
+ else if (starred)
351
+ this.push({ kind: 'skip' });
352
+ }
353
+ /** The object of the innermost object group the reader is in. */
354
+ object() {
355
+ for (let k = this.frames.length - 1; k > 0; k--) {
356
+ const f = this.frames[k];
357
+ if (f.kind === 'object')
358
+ return f.object;
359
+ }
360
+ return undefined;
361
+ }
362
+ quoted(b, at) {
363
+ const top = this.top;
364
+ if (top.kind === 'skip')
365
+ return;
366
+ this.starred = false;
367
+ if (top.kind === 'objdata') {
368
+ top.hex.quote();
369
+ top.alt.quote();
370
+ return;
371
+ }
372
+ if (top.kind === 'objskip') {
373
+ top.data.hex.quote();
374
+ return;
375
+ }
376
+ // A file can end before the two characters of a \'hh.
377
+ const hi = hexValue(b[at] ?? -1);
378
+ const lo = hexValue(b[at + 1] ?? -1);
379
+ if (hi >= 0 && lo >= 0)
380
+ this.char(String.fromCharCode((hi << 4) | lo));
381
+ }
382
+ text(b, from, to) {
383
+ const top = this.top;
384
+ if (top.kind === 'skip')
385
+ return;
386
+ this.starred = false;
387
+ if (top.kind === 'objdata' || top.kind === 'objskip') {
388
+ const both = top.kind === 'objdata';
389
+ const hex = both ? top.hex : top.data.hex;
390
+ for (let k = from; k < to; k++) {
391
+ const v = hexValue(b[k]);
392
+ if (v < 0)
393
+ continue;
394
+ hex.digit(v);
395
+ if (both)
396
+ top.alt.digit(v);
397
+ }
398
+ return;
399
+ }
400
+ // Line ends in RTF text are not text.
401
+ let s = '';
402
+ for (let k = from; k < to; k++) {
403
+ const c = b[k];
404
+ if (c === 0x0a || c === 0x0d)
405
+ continue;
406
+ if (this.skipChars > 0) {
407
+ this.skipChars--;
408
+ continue;
409
+ }
410
+ s += String.fromCharCode(c);
411
+ }
412
+ if (s)
413
+ this.char(s, true);
414
+ }
415
+ /** Text decoded from the file, for the destination being read. */
416
+ char(s, counted = false) {
417
+ if (!counted && this.skipChars > 0) {
418
+ this.skipChars--;
419
+ return;
420
+ }
421
+ const top = this.top;
422
+ if (top.kind === 'fldinst')
423
+ this.fields.text(s);
424
+ else if ((top.kind === 'objclass' || top.kind === 'template' || top.kind === 'hlsrc') && top.text.length < MAX_TEXT)
425
+ top.text += s;
426
+ }
427
+ finishFrame(f) {
428
+ if (f.kind === 'field')
429
+ this.fields.end();
430
+ else if (f.kind === 'objclass') {
431
+ if (f.object)
432
+ f.object.className = f.text.trim();
433
+ }
434
+ else if (f.kind === 'template') {
435
+ const path = f.text.trim();
436
+ if (path)
437
+ this.ctx.external(types_1.OfficeDetail.Template, path, 'template', { source: 'RTF template' }, 'template');
438
+ }
439
+ else if (f.kind === 'hlsrc') {
440
+ const target = f.text.trim();
441
+ if (target && !(0, links_1.isFragment)(target))
442
+ this.ctx.link(target, 'hyperlink', { source: 'RTF shape hyperlink' });
443
+ }
444
+ else if (f.kind === 'objdata') {
445
+ f.object.data = f.hex.result();
446
+ const alt = f.alt.result();
447
+ if (!Buffer.from(alt).equals(Buffer.from(f.object.data)))
448
+ f.object.alt = alt;
449
+ this.decoded += f.object.data.length + (f.object.alt ? alt.length : 0);
450
+ const total = this.ctx.settings.totalDecompressedBytes;
451
+ if (this.decoded > total)
452
+ throw new errors_1.StopError(types_1.OfficeCategory.Limit, types_1.OfficeDetail.TotalDecompressedSize, `The objects decode past ${total} bytes`, { limit: total });
453
+ // An objdata outside any object is an object of its own, as rtfobj reads it.
454
+ if (f.standalone)
455
+ this.pending.push(f.object);
456
+ }
457
+ else if (f.kind === 'object')
458
+ this.pending.push(f.object);
459
+ }
460
+ }
461
+ /** One embedded or linked object: what its OLE 1.0 header names, and what its data holds. */
462
+ async function objectRules(ctx, o) {
463
+ const location = `object at offset ${o.offset}`;
464
+ const kinds = [...o.kinds];
465
+ const base = { object: location, ...(kinds.length ? { kind: kinds.join(' ') } : {}) };
466
+ if (o.update)
467
+ ctx.report(types_1.OfficeCategory.Action, types_1.OfficeDetail.ObjectUpdate, location, base);
468
+ if (o.kinds.has('objocx')) {
469
+ ctx.report(types_1.OfficeCategory.Control, types_1.OfficeDetail.ActiveX, location, { ...base, ...(o.className ? { className: (0, context_1.clip)(o.className) } : {}) });
470
+ return;
471
+ }
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
+ }
477
+ /** An object's data: its OLE 1.0 header, and the Packager data or storage it holds. */
478
+ async function objectData(ctx, o, bytes, location, base) {
479
+ const ole1 = bytes?.length ? (0, oleobj_1.parseOle1)(bytes) : undefined;
480
+ const className = (ole1?.className || o.className).replace(/\0+$/, '');
481
+ const data = { ...base, ...(className ? { className: (0, context_1.clip)(className) } : {}) };
482
+ const link = { object: location, ...(className ? { className: (0, context_1.clip)(className) } : {}) };
483
+ let equation = (0, objects_1.isEquationClass)(className);
484
+ if (ole1?.formatId === 1 || (!bytes?.length && (o.kinds.has('objlink') || o.kinds.has('objautlink')))) {
485
+ const target = ole1?.topicName ?? '';
486
+ if (target)
487
+ ctx.external(types_1.OfficeDetail.Object, target, location, link, 'linked object');
488
+ else
489
+ ctx.report(types_1.OfficeCategory.External, types_1.OfficeDetail.Object, location, data);
490
+ return;
491
+ }
492
+ if (!bytes?.length)
493
+ return;
494
+ if (!ole1)
495
+ data.reason = 'its data is not an OLE 1.0 object';
496
+ const native = ole1?.data;
497
+ if (native && className.toLowerCase() === 'package') {
498
+ const pkg = (0, oleobj_1.parsePackage)(native);
499
+ if (pkg)
500
+ Object.assign(data, (0, objects_1.packageData)(pkg));
501
+ }
502
+ else if (native && native.length >= 8 && cfb_1.CFB_SIGNATURE.every((v, i) => native[i] === v)) {
503
+ let cfb;
504
+ try {
505
+ cfb = await cfb_1.CompoundFile.open((0, io_1.bufferSource)(native), ctx.settings, ctx.settings.deadline);
506
+ }
507
+ catch (e) {
508
+ if (!(e instanceof errors_1.StopError) || e.category !== types_1.OfficeCategory.Container)
509
+ throw e;
510
+ data.reason = `its storage does not read: ${e.message}`;
511
+ }
512
+ if (cfb) {
513
+ (0, objects_1.nestedNotes)(ctx, cfb, location);
514
+ const d = await (0, objects_1.describeObject)(cfb, cfb.root, location);
515
+ if (d.clsid)
516
+ data.clsid = d.clsid;
517
+ if (d.progId)
518
+ data.progId = (0, context_1.clip)(d.progId);
519
+ if (d.pkg)
520
+ Object.assign(data, (0, objects_1.packageData)(d.pkg));
521
+ equation ||= d.equation;
522
+ await (0, objects_1.compoundRules)(ctx, cfb, location, false);
523
+ if (d.linked) {
524
+ (0, objects_1.linkedObject)(ctx, d, location, link);
525
+ if (equation)
526
+ ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Equation, location, data);
527
+ return;
528
+ }
529
+ }
530
+ }
531
+ // OLE2Link is the class of a linked object, CVE-2017-0199's. Its storage names the target, and when the storage does
532
+ // not read, the class still says the object loads from outside.
533
+ if (className.toLowerCase() === 'ole2link')
534
+ ctx.report(types_1.OfficeCategory.External, types_1.OfficeDetail.Object, location, data);
535
+ else
536
+ ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.OleObject, location, data);
537
+ if (equation)
538
+ ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Equation, location, data);
539
+ }
package/dist/types.d.ts CHANGED
@@ -35,6 +35,11 @@ export declare enum OfficeDetail {
35
35
  LeadingBytes = "LEADING_BYTES",
36
36
  HiddenData = "HIDDEN_DATA",
37
37
  TrailingBytes = "TRAILING_BYTES",
38
+ BadHeader = "BAD_HEADER",
39
+ BrokenChain = "BROKEN_CHAIN",
40
+ ChainLoop = "CHAIN_LOOP",
41
+ DirectoryLoop = "DIRECTORY_LOOP",
42
+ BadEntry = "BAD_ENTRY",
38
43
  Doctype = "DOCTYPE",
39
44
  InternalSubset = "INTERNAL_SUBSET",
40
45
  ExternalEntity = "EXTERNAL_ENTITY",
@@ -49,6 +54,7 @@ export declare enum OfficeDetail {
49
54
  XmlEntityDepth = "XML_ENTITY_DEPTH",
50
55
  XmlEntityExpansions = "XML_ENTITY_EXPANSIONS",
51
56
  XmlEntitySize = "XML_ENTITY_SIZE",
57
+ RtfDepth = "RTF_DEPTH",
52
58
  Time = "TIME",
53
59
  Unrecognized = "UNRECOGNIZED",
54
60
  AmbiguousFormat = "AMBIGUOUS_FORMAT",
@@ -58,8 +64,18 @@ export declare enum OfficeDetail {
58
64
  UnreferencedPart = "UNREFERENCED_PART",
59
65
  OdfLayout = "ODF_LAYOUT",
60
66
  PasswordRequired = "PASSWORD_REQUIRED",
67
+ DefaultPassword = "DEFAULT_PASSWORD",
68
+ UserPassword = "USER_PASSWORD",
69
+ Rc4_40 = "RC4_40",
70
+ Rc4_128 = "RC4_128",
71
+ Rc4Other = "RC4_OTHER",
72
+ Aes128 = "AES_128",
73
+ Aes192 = "AES_192",
74
+ Aes256 = "AES_256",
75
+ UnsupportedAlgorithm = "UNSUPPORTED_ALGORITHM",
61
76
  VbaProject = "VBA_PROJECT",
62
77
  XlmMacrosheet = "XLM_MACROSHEET",
78
+ WordBasic = "WORDBASIC",
63
79
  OdfBasic = "ODF_BASIC",
64
80
  OdfScripts = "ODF_SCRIPTS",
65
81
  OdfDialogs = "ODF_DIALOGS",
@@ -71,6 +87,7 @@ export declare enum OfficeDetail {
71
87
  OdfConfiguration = "ODF_CONFIGURATION",
72
88
  RunProgram = "RUN_PROGRAM",
73
89
  OleVerb = "OLE_VERB",
90
+ ObjectUpdate = "OBJECT_UPDATE",
74
91
  Field = "FIELD",
75
92
  Formula = "FORMULA",
76
93
  Macrobutton = "MACROBUTTON",
@@ -100,6 +117,8 @@ export declare enum OfficeDetail {
100
117
  OleFile = "OLE_FILE",
101
118
  Applet = "APPLET",
102
119
  Plugin = "PLUGIN",
120
+ Equation = "EQUATION",
121
+ Attachment = "ATTACHMENT",
103
122
  ActiveX = "ACTIVEX"
104
123
  }
105
124
  export type OfficeFindingAction = 'reject' | 'strip' | 'info';
@@ -183,6 +202,8 @@ export interface OfficeLimits {
183
202
  xmlEntityExpansions?: number;
184
203
  /** Characters of entity text expanded in the whole run. Default 16 Mi. */
185
204
  xmlEntitySize?: number;
205
+ /** Group nesting in an RTF file, and destinations open at once. Default 256. */
206
+ rtfDepth?: number;
186
207
  /** Milliseconds for the whole run. Not enforced unless set. */
187
208
  timeMs?: number;
188
209
  }
@@ -240,6 +261,12 @@ export interface OfficeScoreBands {
240
261
  critical: number;
241
262
  }
242
263
  export interface OfficeOptions {
264
+ /**
265
+ * Tried first on an encrypted file: an encrypted OOXML package, or an .xls workbook. If it does not open the file,
266
+ * the password Excel opens it with by default, VelvetSweatshop, is tried. Pass it in code, never on a command line,
267
+ * where other users can read it.
268
+ */
269
+ password?: string;
243
270
  limits?: OfficeLimits;
244
271
  actionOverrides?: OfficeActionOverride[];
245
272
  scoreWeights?: OfficeScoreWeight[];
package/dist/types.js CHANGED
@@ -41,6 +41,11 @@ var OfficeDetail;
41
41
  OfficeDetail["LeadingBytes"] = "LEADING_BYTES";
42
42
  OfficeDetail["HiddenData"] = "HIDDEN_DATA";
43
43
  OfficeDetail["TrailingBytes"] = "TRAILING_BYTES";
44
+ OfficeDetail["BadHeader"] = "BAD_HEADER";
45
+ OfficeDetail["BrokenChain"] = "BROKEN_CHAIN";
46
+ OfficeDetail["ChainLoop"] = "CHAIN_LOOP";
47
+ OfficeDetail["DirectoryLoop"] = "DIRECTORY_LOOP";
48
+ OfficeDetail["BadEntry"] = "BAD_ENTRY";
44
49
  // XML
45
50
  OfficeDetail["Doctype"] = "DOCTYPE";
46
51
  OfficeDetail["InternalSubset"] = "INTERNAL_SUBSET";
@@ -57,6 +62,7 @@ var OfficeDetail;
57
62
  OfficeDetail["XmlEntityDepth"] = "XML_ENTITY_DEPTH";
58
63
  OfficeDetail["XmlEntityExpansions"] = "XML_ENTITY_EXPANSIONS";
59
64
  OfficeDetail["XmlEntitySize"] = "XML_ENTITY_SIZE";
65
+ OfficeDetail["RtfDepth"] = "RTF_DEPTH";
60
66
  OfficeDetail["Time"] = "TIME";
61
67
  // PACKAGE
62
68
  OfficeDetail["Unrecognized"] = "UNRECOGNIZED";
@@ -68,9 +74,19 @@ var OfficeDetail;
68
74
  OfficeDetail["OdfLayout"] = "ODF_LAYOUT";
69
75
  // ENCRYPTED
70
76
  OfficeDetail["PasswordRequired"] = "PASSWORD_REQUIRED";
77
+ OfficeDetail["DefaultPassword"] = "DEFAULT_PASSWORD";
78
+ OfficeDetail["UserPassword"] = "USER_PASSWORD";
79
+ OfficeDetail["Rc4_40"] = "RC4_40";
80
+ OfficeDetail["Rc4_128"] = "RC4_128";
81
+ OfficeDetail["Rc4Other"] = "RC4_OTHER";
82
+ OfficeDetail["Aes128"] = "AES_128";
83
+ OfficeDetail["Aes192"] = "AES_192";
84
+ OfficeDetail["Aes256"] = "AES_256";
85
+ OfficeDetail["UnsupportedAlgorithm"] = "UNSUPPORTED_ALGORITHM";
71
86
  // MACRO
72
87
  OfficeDetail["VbaProject"] = "VBA_PROJECT";
73
88
  OfficeDetail["XlmMacrosheet"] = "XLM_MACROSHEET";
89
+ OfficeDetail["WordBasic"] = "WORDBASIC";
74
90
  OfficeDetail["OdfBasic"] = "ODF_BASIC";
75
91
  OfficeDetail["OdfScripts"] = "ODF_SCRIPTS";
76
92
  OfficeDetail["OdfDialogs"] = "ODF_DIALOGS";
@@ -84,6 +100,7 @@ var OfficeDetail;
84
100
  // ACTION
85
101
  OfficeDetail["RunProgram"] = "RUN_PROGRAM";
86
102
  OfficeDetail["OleVerb"] = "OLE_VERB";
103
+ OfficeDetail["ObjectUpdate"] = "OBJECT_UPDATE";
87
104
  // DDE
88
105
  OfficeDetail["Field"] = "FIELD";
89
106
  OfficeDetail["Formula"] = "FORMULA";
@@ -118,6 +135,8 @@ var OfficeDetail;
118
135
  OfficeDetail["OleFile"] = "OLE_FILE";
119
136
  OfficeDetail["Applet"] = "APPLET";
120
137
  OfficeDetail["Plugin"] = "PLUGIN";
138
+ OfficeDetail["Equation"] = "EQUATION";
139
+ OfficeDetail["Attachment"] = "ATTACHMENT";
121
140
  // CONTROL
122
141
  OfficeDetail["ActiveX"] = "ACTIVEX";
123
142
  })(OfficeDetail || (exports.OfficeDetail = OfficeDetail = {}));
package/dist/word.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ import type { CfbEntry, CompoundFile } from './cfb';
2
+ import type { RunContext } from './context';
3
+ /**
4
+ * Inspects a Word 6.0 to 2003 document: its FIB, whether it is encrypted, the fields in its text and the template it
5
+ * attaches. Returns whether it is a template, or undefined for an `optional` stream that holds no FIB.
6
+ */
7
+ export declare function wordRules(ctx: RunContext, cfb: CompoundFile, stream: CfbEntry, optional?: boolean): Promise<{
8
+ template: boolean;
9
+ } | undefined>;