@patrickdk77/office-defuse 0.2.0 → 0.2.2

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/types.d.ts CHANGED
@@ -234,6 +234,29 @@ export interface OfficeLimits {
234
234
  /** Milliseconds for the whole run. Not enforced unless set. */
235
235
  timeMs?: number;
236
236
  }
237
+ export interface FormatDetection {
238
+ /** The format's MIME type, such as application/vnd.openxmlformats-officedocument.wordprocessingml.document. */
239
+ mimeType: string;
240
+ /** The format's usual extension, lowercase and without the dot. */
241
+ extension: string;
242
+ /** Where the format starts in the file: 0, or the offset of its data when other data comes first. */
243
+ offset: number;
244
+ /** Set when the match rests on more than the content, and says what it rests on. */
245
+ heuristic?: string;
246
+ }
247
+ /** What a detect function may know of a file besides its content. */
248
+ export interface DetectOptions {
249
+ /** The file's name, as the upload or the container that holds it gives it. */
250
+ name?: string;
251
+ /** The MIME type the upload or the container declares. */
252
+ declaredType?: string;
253
+ /** The Date.now() value detection must finish by, as `PluginContext.deadline`. */
254
+ deadline?: number;
255
+ /** Folder for temporary files. Defaults to `os.tmpdir()`. */
256
+ tempDir?: string;
257
+ /** Bytes held in memory before a temporary file takes over. */
258
+ memoryThreshold?: number;
259
+ }
237
260
  /** What a contained file plugin learns of the run, with nothing format-specific, so any package of the family can make one. */
238
261
  export interface PluginContext {
239
262
  /** The depth of the file itself: 0 for the upload, 1 for a file inside it, and so on, whatever the formats. */
package/dist/word.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import type { CfbEntry, CompoundFile } from './cfb';
2
2
  import type { RunContext } from './context';
3
+ /** Whether a WordDocument stream holds a template: its FIB has fDot set. */
4
+ export declare function isWordTemplate(cfb: CompoundFile, stream: CfbEntry): Promise<boolean>;
3
5
  /**
4
6
  * 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.
7
+ * attaches. An `optional` stream that holds no FIB is not read.
6
8
  */
7
- export declare function wordRules(ctx: RunContext, cfb: CompoundFile, stream: CfbEntry, optional?: boolean): Promise<{
8
- template: boolean;
9
- } | undefined>;
9
+ export declare function wordRules(ctx: RunContext, cfb: CompoundFile, stream: CfbEntry, optional?: boolean): Promise<void>;
package/dist/word.js CHANGED
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isWordTemplate = isWordTemplate;
3
4
  exports.wordRules = wordRules;
4
5
  const errors_1 = require("./errors");
5
6
  const fields_1 = require("./fields");
@@ -26,20 +27,33 @@ const decrypted = (src, decrypt) => ({
26
27
  read: async (offset, length) => decrypt(offset, await src.read(offset, length)),
27
28
  });
28
29
  const u32 = (b, at) => (b[at] | (b[at + 1] << 8) | (b[at + 2] << 16) | (b[at + 3] << 24)) >>> 0;
30
+ /**
31
+ * The identifier and flags of the FIB a WordDocument stream starts with (MS-DOC 2.5.2), or undefined when it does not
32
+ * start with the FIB of Word 6.0 to 2003. The FIB's base is never encrypted.
33
+ */
34
+ function fibBase(fib) {
35
+ const ident = fib.length >= 32 ? u16(fib, 0) : 0;
36
+ return ident === WORD8 || ident === WORD6 ? { ident, flags: u16(fib, 10) } : undefined;
37
+ }
38
+ /** Whether a WordDocument stream holds a template: its FIB has fDot set. */
39
+ async function isWordTemplate(cfb, stream) {
40
+ const base = fibBase(await cfb.source(stream).read(0, 32));
41
+ return base !== undefined && (base.flags & 1) === 1;
42
+ }
29
43
  /**
30
44
  * Inspects a Word 6.0 to 2003 document: its FIB, whether it is encrypted, the fields in its text and the template it
31
- * attaches. Returns whether it is a template, or undefined for an `optional` stream that holds no FIB.
45
+ * attaches. An `optional` stream that holds no FIB is not read.
32
46
  */
33
47
  async function wordRules(ctx, cfb, stream, optional = false) {
34
48
  let source = cfb.source(stream);
35
49
  let fib = await source.read(0, 1024);
36
- const ident = fib.length >= 32 ? u16(fib, 0) : 0;
37
- if (ident !== WORD8 && ident !== WORD6) {
50
+ const base = fibBase(fib);
51
+ if (!base) {
38
52
  if (optional)
39
- return undefined;
53
+ return;
40
54
  throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.Unrecognized, 'The WordDocument stream does not start with a Word FIB', { reason: 'a WordDocument stream without a FIB' });
41
55
  }
42
- const flags = u16(fib, 10);
56
+ const { ident, flags } = base;
43
57
  const tableEntry = cfb.child(cfb.root, flags & 0x0200 ? '1Table' : '0Table');
44
58
  let tableSource = tableEntry?.kind === 'stream' ? cfb.source(tableEntry) : undefined;
45
59
  // fEncrypted, set for RC4, RC4 CryptoAPI and XOR obfuscation alike. Word 97 to 2003 documents are read through the
@@ -126,7 +140,6 @@ async function wordRules(ctx, cfb, stream, optional = false) {
126
140
  }
127
141
  }
128
142
  fields.finish();
129
- return { template: (flags & 1) === 1 };
130
143
  }
131
144
  /** The fc and lcb pairs of a Word 97 FIB, after its variable-length arrays of 16- and 32-bit values. */
132
145
  function fcLcb(fib) {
package/dist/xls.d.ts CHANGED
@@ -8,16 +8,22 @@ export type Decrypt = (r: BiffRecord) => Uint8Array;
8
8
  * reads workbooks without knowing how they are encrypted.
9
9
  */
10
10
  export type Unlock = (ctx: RunContext, filePass: BiffRecord, stream: string) => Decrypt | Promise<Decrypt>;
11
+ /**
12
+ * Whether a workbook stream holds a template or an add-in: its globals substream, from the BOF that starts the stream
13
+ * to its EOF, holds a Template or an AddIn record. Record headers are never encrypted, so an encrypted workbook tells
14
+ * this without its password. A stream that does not start with a BOF record is neither.
15
+ */
16
+ export declare function workbookKind(cfb: CompoundFile, stream: CfbEntry, deadline?: number): Promise<{
17
+ template: boolean;
18
+ addIn: boolean;
19
+ }>;
11
20
  /**
12
21
  * Inspects an Excel 5.0 to 2003 workbook stream. Excel reads the globals substream from the start of the stream,
13
22
  * then each sheet from where its BoundSheet record says the sheet starts. The globals give the macro sheets, the
14
23
  * names Excel runs as macros, and the DDE, OLE and workbook links. The sheets give shapes that run macros and
15
24
  * hyperlinks. An encrypted workbook goes through `unlock`.
16
25
  */
17
- export declare function excelRules(ctx: RunContext, cfb: CompoundFile, stream: CfbEntry, unlock: Unlock): Promise<{
18
- template: boolean;
19
- addIn: boolean;
20
- }>;
26
+ export declare function excelRules(ctx: RunContext, cfb: CompoundFile, stream: CfbEntry, unlock: Unlock): Promise<void>;
21
27
  /**
22
28
  * A VirtualPath (MS-XLS 2.5.277) written out. One that starts with U+0001 is encoded: its control characters name a
23
29
  * volume, the root, a folder, a step up, a URL's volume or Excel's startup folders.
package/dist/xls.js CHANGED
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.workbookKind = workbookKind;
3
4
  exports.excelRules = excelRules;
4
5
  exports.virtualPath = virtualPath;
5
6
  exports.hyperlink = hyperlink;
@@ -21,6 +22,25 @@ const FORMULA_AT = new Map([
21
22
  [SHRFMLA, 8],
22
23
  [ARRAY, 12],
23
24
  ]);
25
+ /**
26
+ * Whether a workbook stream holds a template or an add-in: its globals substream, from the BOF that starts the stream
27
+ * to its EOF, holds a Template or an AddIn record. Record headers are never encrypted, so an encrypted workbook tells
28
+ * this without its password. A stream that does not start with a BOF record is neither.
29
+ */
30
+ async function workbookKind(cfb, stream, deadline) {
31
+ const kind = { template: false, addIn: false };
32
+ const records = new biff_1.RecordReader(cfb, stream, 0, deadline);
33
+ const first = await records.next();
34
+ if (first?.type !== biff_1.R.bof || first.body.length < 4)
35
+ return kind;
36
+ for (let r = await records.next(); r && r.type !== biff_1.R.eof; r = await records.next()) {
37
+ if (r.type === biff_1.R.template)
38
+ kind.template = true;
39
+ else if (r.type === biff_1.R.addIn)
40
+ kind.addIn = true;
41
+ }
42
+ return kind;
43
+ }
24
44
  /**
25
45
  * Inspects an Excel 5.0 to 2003 workbook stream. Excel reads the globals substream from the start of the stream,
26
46
  * then each sheet from where its BoundSheet record says the sheet starts. The globals give the macro sheets, the
@@ -40,7 +60,6 @@ async function excelRules(ctx, cfb, stream, unlock) {
40
60
  const names = [];
41
61
  /** The SupBook each XTI of the ExternSheet record names. */
42
62
  const xti = [];
43
- const flags = { template: false, addIn: false };
44
63
  for (let r = await records.next(); r && r.type !== biff_1.R.eof; r = await records.next()) {
45
64
  if (r.type === biff_1.R.filePass) {
46
65
  decrypt = await unlock(ctx, r, location);
@@ -49,10 +68,6 @@ async function excelRules(ctx, cfb, stream, unlock) {
49
68
  const body = decrypt(r);
50
69
  if (r.type === biff_1.R.boundSheet)
51
70
  sheets.push((0, biff_1.parseBoundSheet)(body, biff8));
52
- else if (r.type === biff_1.R.template)
53
- flags.template = true;
54
- else if (r.type === biff_1.R.addIn)
55
- flags.addIn = true;
56
71
  else if (r.type === biff_1.R.lbl) {
57
72
  const name = (0, biff_1.parseLbl)(body, biff8);
58
73
  names.push(name.name);
@@ -85,7 +100,6 @@ async function excelRules(ctx, cfb, stream, unlock) {
85
100
  const nameOf = (ref) => (ref.kind === 'name' ? names[ref.index - 1] : books[xti[ref.ixti] ?? -1]?.names[ref.index - 1]);
86
101
  for (const s of sheets)
87
102
  await sheetRules(ctx, cfb, stream, s, decrypt, location, biff8 ? nameOf : undefined);
88
- return flags;
89
103
  }
90
104
  /** A SupBook record, read as MS-XLS 2.4.271 and oletools tell its kinds apart. */
91
105
  function linkRules(ctx, b, location) {
package/dist/xml.d.ts CHANGED
@@ -159,6 +159,9 @@ export declare class XmlTokenizer {
159
159
  /** The XML declaration says standalone="yes". */
160
160
  private standalone;
161
161
  private encoding;
162
+ /** Set by a declaration of US-ASCII, which agrees with UTF-8 only while every byte is ASCII. */
163
+ private asciiDeclared;
164
+ private nonAscii;
162
165
  /** Start tags read so far, which numbers the elements. */
163
166
  private elements;
164
167
  /** An attribute value of the tag being read referred to an entity of the internal subset. */
package/dist/xml.js CHANGED
@@ -190,6 +190,9 @@ class XmlTokenizer {
190
190
  /** The XML declaration says standalone="yes". */
191
191
  standalone = false;
192
192
  encoding = 'utf-8';
193
+ /** Set by a declaration of US-ASCII, which agrees with UTF-8 only while every byte is ASCII. */
194
+ asciiDeclared = false;
195
+ nonAscii = false;
193
196
  /** Start tags read so far, which numbers the elements. */
194
197
  elements = 0;
195
198
  /** An attribute value of the tag being read referred to an entity of the internal subset. */
@@ -263,6 +266,14 @@ class XmlTokenizer {
263
266
  return decoder;
264
267
  }
265
268
  decode(decoder, bytes, more) {
269
+ if (!this.nonAscii)
270
+ for (const c of bytes)
271
+ if (c >= 0x80) {
272
+ this.nonAscii = true;
273
+ break;
274
+ }
275
+ if (this.asciiDeclared && this.nonAscii)
276
+ this.stop(types_1.OfficeDetail.Encoding, 'The declaration says us-ascii, the bytes are not all ASCII');
266
277
  try {
267
278
  return decoder.decode(bytes, { stream: more });
268
279
  }
@@ -279,9 +290,16 @@ class XmlTokenizer {
279
290
  const declared = /\bencoding\s*=\s*(["'])([^"']*)\1/.exec(body)?.[2]?.toLowerCase();
280
291
  if (declared === undefined)
281
292
  return;
282
- const ok = this.encoding === 'utf-8' ? declared === 'utf-8' || declared === 'utf8' : declared === 'utf-16' || declared === this.encoding;
293
+ // SharePoint declares us-ascii on the custom XML it writes. ASCII bytes read the same in UTF-8.
294
+ const ascii = this.encoding === 'utf-8' && (declared === 'us-ascii' || declared === 'ascii');
295
+ const ok = this.encoding === 'utf-8' ? declared === 'utf-8' || declared === 'utf8' || ascii : declared === 'utf-16' || declared === this.encoding;
283
296
  if (!ok)
284
297
  this.stop(types_1.OfficeDetail.Encoding, `The declaration says ${declared}, the bytes are ${this.encoding.toUpperCase()}`);
298
+ if (ascii) {
299
+ this.asciiDeclared = true;
300
+ if (this.nonAscii)
301
+ this.stop(types_1.OfficeDetail.Encoding, 'The declaration says us-ascii, the bytes are not all ASCII');
302
+ }
285
303
  }
286
304
  feed(text, final) {
287
305
  this.buf = this.pos < this.buf.length ? this.buf.slice(this.pos) + text : text;
package/dist/zip.d.ts CHANGED
@@ -48,6 +48,8 @@ export declare class ZipArchive {
48
48
  readonly entries: ZipEntry[];
49
49
  readonly notes: ZipNote[];
50
50
  private readonly deadline;
51
+ /** Where the archive's data starts: the first local header, or the central directory of an archive without entries. */
52
+ readonly start: number;
51
53
  private readonly byKey;
52
54
  private constructor();
53
55
  /** The entry with this name, compared as entry names collide: ignoring case. */
package/dist/zip.js CHANGED
@@ -241,12 +241,16 @@ class ZipArchive {
241
241
  entries;
242
242
  notes;
243
243
  deadline;
244
+ start;
244
245
  byKey = new Map();
245
- constructor(reader, entries, notes, deadline) {
246
+ constructor(reader, entries, notes, deadline,
247
+ /** Where the archive's data starts: the first local header, or the central directory of an archive without entries. */
248
+ start) {
246
249
  this.reader = reader;
247
250
  this.entries = entries;
248
251
  this.notes = notes;
249
252
  this.deadline = deadline;
253
+ this.start = start;
250
254
  for (const e of entries)
251
255
  this.byKey.set(nameKey(e.name), e);
252
256
  }
@@ -447,7 +451,7 @@ class ZipArchive {
447
451
  if (at !== undefined)
448
452
  notes.push({ detail: types_1.OfficeDetail.HiddenEntry, bytes: to - from, offset: at });
449
453
  }
450
- return new ZipArchive(reader, entries, notes, deadline);
454
+ return new ZipArchive(reader, entries, notes, deadline, sorted.length ? sorted[0].localOffset : dirOffset);
451
455
  }
452
456
  /** Checks a local header and any data descriptor against the central directory, and fills in where the entry's data and the entry end. */
453
457
  static async checkLocal(reader, e, rawName, dirOffset) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@patrickdk77/office-defuse",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Inspect Office and OpenDocument files for VBA and Excel 4.0 macros, LibreOffice Basic and scripts, DDE, external templates and links, embedded OLE objects and ActiveX controls, and score the risk. Strict ZIP and XML readers, bounded memory, no runtime dependencies.",
5
5
  "license": "MIT",
6
6
  "author": "patrickdk77",