@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/crc32.js CHANGED
@@ -1,7 +1,41 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.crc32 = crc32;
4
- // zlib.crc32 arrived in Node 22.2, and the package supports every Node 22.
36
+ exports.crc32 = void 0;
37
+ exports.tableCrc32 = tableCrc32;
38
+ const zlib = __importStar(require("node:zlib"));
5
39
  const TABLE = new Uint32Array(256);
6
40
  for (let n = 0; n < 256; n++) {
7
41
  let c = n;
@@ -9,10 +43,16 @@ for (let n = 0; n < 256; n++) {
9
43
  c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
10
44
  TABLE[n] = c >>> 0;
11
45
  }
12
- /** Continues a CRC-32, the one ZIP uses, over `bytes`. Start from 0. */
13
- function crc32(bytes, crc = 0) {
46
+ /** Continues a CRC-32, the one ZIP uses, over `bytes`, with a lookup table. Start from 0. */
47
+ function tableCrc32(bytes, crc = 0) {
14
48
  let c = ~crc >>> 0;
15
49
  for (const b of bytes)
16
50
  c = TABLE[(c ^ b) & 0xff] ^ (c >>> 8);
17
51
  return ~c >>> 0;
18
52
  }
53
+ /**
54
+ * Continues a CRC-32, the one ZIP uses, over `bytes`. Start from 0. This is zlib.crc32 where Node has it, about 50
55
+ * times faster, and the table otherwise. Node 18 and early releases of Node 20 and 22 lack it. The choice is made
56
+ * once, when the module loads.
57
+ */
58
+ exports.crc32 = zlib.crc32 ?? tableCrc32;
package/dist/engine.js CHANGED
@@ -2,12 +2,14 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.inspectOfficeSource = inspectOfficeSource;
4
4
  exports.inspectOffice = inspectOffice;
5
+ const cfb_1 = require("./cfb");
5
6
  const context_1 = require("./context");
6
7
  const errors_1 = require("./errors");
7
8
  const findings_1 = require("./findings");
8
9
  const io_1 = require("./io");
9
- const odf_1 = require("./odf");
10
- const ooxml_1 = require("./ooxml");
10
+ const ole_1 = require("./ole");
11
+ const package_1 = require("./package");
12
+ const rtf_1 = require("./rtf");
11
13
  const score_1 = require("./score");
12
14
  const types_1 = require("./types");
13
15
  const zip_1 = require("./zip");
@@ -20,6 +22,7 @@ const DEFAULT_LIMITS = {
20
22
  xmlEntityDepth: 16,
21
23
  xmlEntityExpansions: 100_000,
22
24
  xmlEntitySize: 16 * 1024 * 1024,
25
+ rtfDepth: 256,
23
26
  };
24
27
  function resolveLimits(given = {}) {
25
28
  return {
@@ -31,6 +34,7 @@ function resolveLimits(given = {}) {
31
34
  xmlEntityDepth: given.xmlEntityDepth ?? DEFAULT_LIMITS.xmlEntityDepth,
32
35
  xmlEntityExpansions: given.xmlEntityExpansions ?? DEFAULT_LIMITS.xmlEntityExpansions,
33
36
  xmlEntitySize: given.xmlEntitySize ?? DEFAULT_LIMITS.xmlEntitySize,
37
+ rtfDepth: given.rtfDepth ?? DEFAULT_LIMITS.rtfDepth,
34
38
  fileSize: given.fileSize,
35
39
  timeMs: given.timeMs,
36
40
  };
@@ -82,6 +86,44 @@ function guarded(source) {
82
86
  };
83
87
  return g;
84
88
  }
89
+ const ambiguous = (reason) => new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.AmbiguousFormat, `The file is ${reason}`, { reason });
90
+ /**
91
+ * Reads the file's container. A file that starts with the compound file signature is read as one, and one that
92
+ * starts with {\rt is RTF, which is how Word tells them. The ZIP reader runs too, from the end: a file both read is
93
+ * ambiguous, since Office goes by the first bytes and a ZIP reader by the last. A compound file that does not read is
94
+ * a ZIP archive with data in front, if the ZIP reader read one. Word reads any file that starts with {\rt as RTF, so
95
+ * that does not fail.
96
+ */
97
+ async function openContainer(src, size, limits, settings) {
98
+ const head = await src.read(0, Math.min(8, size));
99
+ const cfbSignature = head.length === 8 && head.every((b, i) => b === cfb_1.CFB_SIGNATURE[i]);
100
+ const rtfSignature = Buffer.from(head.subarray(0, 4)).toString('latin1') === '{\\rt';
101
+ const probe = cfbSignature || rtfSignature
102
+ ? await zip_1.ZipArchive.open(src, limits, settings.deadline).catch((e) => {
103
+ if (e instanceof errors_1.StopError)
104
+ return undefined;
105
+ throw e;
106
+ })
107
+ : undefined;
108
+ if (rtfSignature) {
109
+ if (probe)
110
+ throw ambiguous('both RTF and a ZIP archive');
111
+ return { kind: 'rtf' };
112
+ }
113
+ if (cfbSignature) {
114
+ try {
115
+ const cfb = await cfb_1.CompoundFile.open(src, settings, settings.deadline);
116
+ if (probe)
117
+ throw ambiguous('both a compound file and a ZIP archive');
118
+ return { kind: 'cfb', cfb };
119
+ }
120
+ catch (e) {
121
+ if (!probe || !(e instanceof errors_1.StopError) || e.category !== types_1.OfficeCategory.Container)
122
+ throw e;
123
+ }
124
+ }
125
+ return { kind: 'zip', zip: probe ?? (await zip_1.ZipArchive.open(src, limits, settings.deadline)) };
126
+ }
85
127
  /** Inspects an Office or OpenDocument file without changing it. The source is closed when the run ends. */
86
128
  async function inspectOfficeSource(source, options = {}) {
87
129
  const limits = resolveLimits(options.limits);
@@ -89,40 +131,47 @@ async function inspectOfficeSource(source, options = {}) {
89
131
  const factory = new findings_1.FindingFactory(options.actionOverrides);
90
132
  const findings = [];
91
133
  const src = guarded(source);
92
- let ctx;
134
+ const settings = {
135
+ entries: limits.entries,
136
+ decompressedBytes: limits.decompressedBytes,
137
+ totalDecompressedBytes: limits.totalDecompressedBytes,
138
+ rtfDepth: limits.rtfDepth,
139
+ deadline,
140
+ password: options.password,
141
+ };
142
+ const ctx = new context_1.RunContext(factory, {
143
+ depth: limits.xmlDepth,
144
+ tokenSize: limits.xmlTokenSize,
145
+ entityDepth: limits.xmlEntityDepth,
146
+ entityExpansions: limits.xmlEntityExpansions,
147
+ entitySize: limits.xmlEntitySize,
148
+ }, settings);
93
149
  try {
94
150
  const size = await src.size();
95
151
  if (limits.fileSize !== undefined && size > limits.fileSize)
96
152
  throw new errors_1.StopError(types_1.OfficeCategory.Limit, types_1.OfficeDetail.FileSize, `${size} bytes`, { size, limit: limits.fileSize });
97
153
  (0, errors_1.checkTime)(deadline);
98
- const zip = await zip_1.ZipArchive.open(src, limits, deadline);
99
- for (const n of zip.notes)
100
- findings.push(factory.make(types_1.OfficeCategory.Container, n.detail, undefined, { bytes: n.bytes }));
101
- ctx = new context_1.RunContext(zip, factory, {
102
- depth: limits.xmlDepth,
103
- tokenSize: limits.xmlTokenSize,
104
- entityDepth: limits.xmlEntityDepth,
105
- entityExpansions: limits.xmlEntityExpansions,
106
- entitySize: limits.xmlEntitySize,
107
- });
108
- const contentTypes = zip.get('[Content_Types].xml');
109
- const odf = zip.entries.some(e => e.name === 'mimetype') || zip.get('META-INF/manifest.xml') !== undefined;
110
- if (contentTypes && odf)
111
- throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.AmbiguousFormat, 'The file has both [Content_Types].xml and an OpenDocument mimetype or manifest', { reason: 'OOXML and OpenDocument markers' });
112
- if (contentTypes)
113
- await (0, ooxml_1.inspectOoxml)(ctx, contentTypes);
114
- else if (odf)
115
- await (0, odf_1.inspectOdf)(ctx);
116
- else
117
- throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.Unrecognized, 'The archive has neither [Content_Types].xml nor an OpenDocument mimetype or manifest', { reason: 'a plain ZIP archive' });
154
+ const container = await openContainer(src, size, limits, settings);
155
+ if (container.kind === 'rtf')
156
+ await (0, rtf_1.inspectRtf)(ctx, src, size);
157
+ else if (container.kind === 'cfb') {
158
+ for (const n of container.cfb.notes)
159
+ findings.push(factory.make(types_1.OfficeCategory.Container, n.detail, undefined, { bytes: n.bytes }));
160
+ await (0, ole_1.inspectCompound)(ctx, container.cfb);
161
+ }
162
+ else {
163
+ const zip = container.zip;
164
+ for (const n of zip.notes)
165
+ findings.push(factory.make(types_1.OfficeCategory.Container, n.detail, undefined, { bytes: n.bytes }));
166
+ await (0, package_1.inspectPackage)(ctx.withZip(zip));
167
+ }
118
168
  findings.push(...ctx.findings);
119
169
  return finish(findings, options, false, ctx.format);
120
170
  }
121
171
  catch (e) {
122
172
  if (src.failure !== undefined)
123
173
  throw src.failure;
124
- if (ctx)
125
- findings.push(...ctx.findings);
174
+ findings.push(...ctx.findings);
126
175
  if (e instanceof errors_1.StopError) {
127
176
  findings.push(factory.make(e.category, e.detail, e.location ?? (typeof e.data?.name === 'string' ? e.data.name : undefined), { ...e.data, reason: e.data?.reason ?? e.message }));
128
177
  }
@@ -134,7 +183,7 @@ async function inspectOfficeSource(source, options = {}) {
134
183
  // Anything unexpected rejects the file rather than escaping as an exception.
135
184
  findings.push(factory.make(types_1.OfficeCategory.Package, types_1.OfficeDetail.Unreadable, undefined, { reason: String(e?.message ?? e).slice(0, 200) }));
136
185
  }
137
- return finish(findings, options, true, ctx?.format);
186
+ return finish(findings, options, true, ctx.format);
138
187
  }
139
188
  finally {
140
189
  try {
@@ -0,0 +1,29 @@
1
+ import { type RunContext } from './context';
2
+ /**
3
+ * Reports a Word field by its code. A field whose name holds the content of an external entity has a type the file
4
+ * does not say, as one whose name another field builds does.
5
+ */
6
+ export declare function evaluateField(ctx: RunContext, location: string, code: string, computed: boolean): void;
7
+ /**
8
+ * Word field codes, followed through nested fields as the text of a document arrives: a field begins, its code
9
+ * comes, a separator starts its result, and it ends. Each format says where those marks are.
10
+ */
11
+ export declare class FieldStack {
12
+ private readonly ctx;
13
+ private readonly location;
14
+ private readonly stack;
15
+ constructor(ctx: RunContext, location: string);
16
+ /** A field starts. One that starts before its parent's code has a word in it makes that code at run time. */
17
+ begin(): void;
18
+ /** A field that is all code, such as OOXML's fldSimple. */
19
+ simple(code: string): void;
20
+ separate(): void;
21
+ end(): void;
22
+ /** Whether a field's code is being read now. */
23
+ get inCode(): boolean;
24
+ /** Text of the innermost field's code. White space before the first word counts for nothing, so no amount of it can push the field's name out. */
25
+ text(t: string): void;
26
+ /** A document can end inside a field. Word still reads the code it has. */
27
+ finish(): void;
28
+ private nest;
29
+ }
package/dist/fields.js ADDED
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FieldStack = void 0;
4
+ exports.evaluateField = evaluateField;
5
+ const context_1 = require("./context");
6
+ const formulas_1 = require("./formulas");
7
+ const links_1 = require("./links");
8
+ const types_1 = require("./types");
9
+ const xml_1 = require("./xml");
10
+ /** How much of a field code is kept, counted from its first word, which names the field. */
11
+ const MAX_FIELD_CODE = 4096;
12
+ /**
13
+ * Reports a Word field by its code. A field whose name holds the content of an external entity has a type the file
14
+ * does not say, as one whose name another field builds does.
15
+ */
16
+ function evaluateField(ctx, location, code, computed) {
17
+ const data = { code: (0, context_1.clip)(code.trim()) };
18
+ const words = (0, formulas_1.fieldWords)(code);
19
+ const first = words[0]?.text ?? '';
20
+ const entity = (0, xml_1.unknownEntity)(first);
21
+ if (computed || entity !== undefined)
22
+ ctx.report(types_1.OfficeCategory.Field, types_1.OfficeDetail.Computed, location, entity === undefined ? data : { ...data, entity: (0, context_1.clip)(entity) });
23
+ const name = first.toUpperCase();
24
+ // An argument comes before any switch, and a switch is an unquoted word starting with a backslash.
25
+ const switchAt = words.findIndex(w => !w.quoted && w.text.startsWith('\\'));
26
+ const arg = (n) => (words[n] !== undefined && (switchAt < 0 || n < switchAt) ? words[n].text : undefined);
27
+ if (name === 'DDE' || name === 'DDEAUTO')
28
+ ctx.report(types_1.OfficeCategory.Dde, types_1.OfficeDetail.Field, location, data);
29
+ else if (name === 'MACROBUTTON')
30
+ ctx.report(types_1.OfficeCategory.Field, types_1.OfficeDetail.Macrobutton, location, { ...data, macro: (0, context_1.clip)(words[1]?.text ?? '') });
31
+ else if (name === 'INCLUDETEXT' || name === 'INCLUDEPICTURE' || name === 'IMPORT' || name === 'LINK') {
32
+ const target = arg(name === 'LINK' ? 2 : 1);
33
+ if (target === undefined)
34
+ ctx.report(types_1.OfficeCategory.External, types_1.OfficeDetail.Include, location, data);
35
+ else
36
+ ctx.external(types_1.OfficeDetail.Include, target, location, data, 'field');
37
+ }
38
+ else if (name === 'HYPERLINK') {
39
+ const target = arg(1);
40
+ if (target !== undefined && !(0, links_1.isFragment)(target))
41
+ ctx.link(target, location, { ...data, source: 'HYPERLINK field' });
42
+ }
43
+ }
44
+ /**
45
+ * Word field codes, followed through nested fields as the text of a document arrives: a field begins, its code
46
+ * comes, a separator starts its result, and it ends. Each format says where those marks are.
47
+ */
48
+ class FieldStack {
49
+ ctx;
50
+ location;
51
+ stack = [];
52
+ constructor(ctx, location) {
53
+ this.ctx = ctx;
54
+ this.location = location;
55
+ }
56
+ /** A field starts. One that starts before its parent's code has a word in it makes that code at run time. */
57
+ begin() {
58
+ this.nest();
59
+ this.stack.push({ code: '', inCode: true, computed: false });
60
+ }
61
+ /** A field that is all code, such as OOXML's fldSimple. */
62
+ simple(code) {
63
+ this.nest();
64
+ evaluateField(this.ctx, this.location, code, false);
65
+ }
66
+ separate() {
67
+ const top = this.stack[this.stack.length - 1];
68
+ if (top)
69
+ top.inCode = false;
70
+ }
71
+ end() {
72
+ const f = this.stack.pop();
73
+ if (f)
74
+ evaluateField(this.ctx, this.location, f.code, f.computed);
75
+ }
76
+ /** Whether a field's code is being read now. */
77
+ get inCode() {
78
+ return this.stack[this.stack.length - 1]?.inCode === true;
79
+ }
80
+ /** Text of the innermost field's code. White space before the first word counts for nothing, so no amount of it can push the field's name out. */
81
+ text(t) {
82
+ const top = this.stack[this.stack.length - 1];
83
+ if (top?.inCode && top.code.length < MAX_FIELD_CODE)
84
+ top.code = top.code ? top.code + t : t.trimStart();
85
+ }
86
+ /** A document can end inside a field. Word still reads the code it has. */
87
+ finish() {
88
+ for (let f = this.stack.pop(); f; f = this.stack.pop())
89
+ evaluateField(this.ctx, this.location, f.code, f.computed);
90
+ }
91
+ nest() {
92
+ const top = this.stack[this.stack.length - 1];
93
+ if (top?.inCode && top.code === '')
94
+ top.computed = true;
95
+ }
96
+ }
97
+ exports.FieldStack = FieldStack;
package/dist/findings.js CHANGED
@@ -13,8 +13,8 @@ def(types_1.OfficeCategory.Container, types_1.OfficeDetail.NotZip, 'reject', 0,
13
13
  def(types_1.OfficeCategory.Container, types_1.OfficeDetail.AmbiguousEnd, 'reject', 0, 'The archive has more than one end record a reader could pick');
14
14
  def(types_1.OfficeCategory.Container, types_1.OfficeDetail.MultiDisk, 'reject', 0, 'The archive claims to span several disks');
15
15
  def(types_1.OfficeCategory.Container, types_1.OfficeDetail.DirectoryMismatch, 'reject', 0, "The archive's end record disagrees with its central directory");
16
- def(types_1.OfficeCategory.Container, types_1.OfficeDetail.OutOfRange, 'reject', 0, 'An entry or the central directory lies outside the file');
17
- def(types_1.OfficeCategory.Container, types_1.OfficeDetail.Overlap, 'reject', 0, 'Two entries in the archive share bytes');
16
+ def(types_1.OfficeCategory.Container, types_1.OfficeDetail.OutOfRange, 'reject', 0, 'An entry, a sector or a directory lies outside the file');
17
+ def(types_1.OfficeCategory.Container, types_1.OfficeDetail.Overlap, 'reject', 0, 'Two entries or streams of the file share bytes');
18
18
  def(types_1.OfficeCategory.Container, types_1.OfficeDetail.HeaderMismatch, 'reject', 0, 'A local header contradicts the central directory<: {name}|>');
19
19
  def(types_1.OfficeCategory.Container, types_1.OfficeDetail.DuplicateName, 'reject', 0, 'Two entries have the same name, ignoring case<: {name}|>');
20
20
  def(types_1.OfficeCategory.Container, types_1.OfficeDetail.UnsafeName, 'reject', 0, 'An entry name is unsafe to use as a path<: {name}|>');
@@ -23,10 +23,15 @@ def(types_1.OfficeCategory.Container, types_1.OfficeDetail.EncryptedEntry, 'reje
23
23
  def(types_1.OfficeCategory.Container, types_1.OfficeDetail.UnsupportedMethod, 'reject', 0, 'An entry uses a compression method other than stored or deflate<: {name}|>');
24
24
  def(types_1.OfficeCategory.Container, types_1.OfficeDetail.Zip64Mismatch, 'reject', 0, 'ZIP64 fields disagree with the fields they extend');
25
25
  def(types_1.OfficeCategory.Container, types_1.OfficeDetail.BadExtraField, 'reject', 0, 'An extra field block does not parse<: {name}|>');
26
- def(types_1.OfficeCategory.Container, types_1.OfficeDetail.DataError, 'reject', 0, "An entry's data does not match its declared size and checksum<: {name}|>");
26
+ def(types_1.OfficeCategory.Container, types_1.OfficeDetail.DataError, 'reject', 0, "An entry's data does not match its declared size or checksum<: {name}|>");
27
27
  def(types_1.OfficeCategory.Container, types_1.OfficeDetail.LeadingBytes, 'strip', 10, 'Data before the first entry of the archive');
28
- def(types_1.OfficeCategory.Container, types_1.OfficeDetail.HiddenData, 'strip', 20, 'Data between entries of the archive that no entry covers');
29
- def(types_1.OfficeCategory.Container, types_1.OfficeDetail.TrailingBytes, 'strip', 10, 'Data after the end of the archive');
28
+ def(types_1.OfficeCategory.Container, types_1.OfficeDetail.HiddenData, 'strip', 20, 'Data in the file that no entry or stream covers');
29
+ def(types_1.OfficeCategory.Container, types_1.OfficeDetail.TrailingBytes, 'strip', 10, 'Data after the end of the archive or compound file');
30
+ def(types_1.OfficeCategory.Container, types_1.OfficeDetail.BadHeader, 'reject', 0, 'The compound file header is invalid<: {reason}|>');
31
+ def(types_1.OfficeCategory.Container, types_1.OfficeDetail.BrokenChain, 'reject', 0, 'A sector chain of the compound file runs into a free or reserved sector');
32
+ def(types_1.OfficeCategory.Container, types_1.OfficeDetail.ChainLoop, 'reject', 0, 'A sector chain of the compound file loops');
33
+ def(types_1.OfficeCategory.Container, types_1.OfficeDetail.DirectoryLoop, 'reject', 0, 'The compound file directory reaches one entry twice');
34
+ def(types_1.OfficeCategory.Container, types_1.OfficeDetail.BadEntry, 'reject', 0, 'A compound file directory entry is invalid<: {reason}|>');
30
35
  // XML. MALFORMED and ENCODING stop the run.
31
36
  def(types_1.OfficeCategory.Xml, types_1.OfficeDetail.Doctype, 'info', 0, 'An XML part declares a document type<: {part}|>');
32
37
  def(types_1.OfficeCategory.Xml, types_1.OfficeDetail.InternalSubset, 'strip', 20, 'An XML part declares entities or other markup in its document type<: {part}|>');
@@ -35,17 +40,18 @@ def(types_1.OfficeCategory.Xml, types_1.OfficeDetail.Malformed, 'reject', 0, 'An
35
40
  def(types_1.OfficeCategory.Xml, types_1.OfficeDetail.Encoding, 'reject', 0, 'An XML part is not in valid UTF-8 or UTF-16<: {part}|>');
36
41
  // LIMIT
37
42
  def(types_1.OfficeCategory.Limit, types_1.OfficeDetail.FileSize, 'reject', 0, 'The file exceeds the file size limit');
38
- def(types_1.OfficeCategory.Limit, types_1.OfficeDetail.EntryCount, 'reject', 0, 'The archive exceeds the entry count limit');
43
+ def(types_1.OfficeCategory.Limit, types_1.OfficeDetail.EntryCount, 'reject', 0, 'The file exceeds the entry count limit');
39
44
  def(types_1.OfficeCategory.Limit, types_1.OfficeDetail.DecompressedSize, 'reject', 0, 'An entry exceeds the decompressed size limit');
40
- def(types_1.OfficeCategory.Limit, types_1.OfficeDetail.TotalDecompressedSize, 'reject', 0, 'The archive exceeds the total decompressed size limit');
45
+ def(types_1.OfficeCategory.Limit, types_1.OfficeDetail.TotalDecompressedSize, 'reject', 0, 'The file exceeds the total decompressed size limit');
41
46
  def(types_1.OfficeCategory.Limit, types_1.OfficeDetail.XmlDepth, 'reject', 0, 'An XML part exceeds the element depth limit');
42
47
  def(types_1.OfficeCategory.Limit, types_1.OfficeDetail.XmlTokenSize, 'reject', 0, 'An XML part exceeds the token size limit');
43
48
  def(types_1.OfficeCategory.Limit, types_1.OfficeDetail.XmlEntityDepth, 'reject', 0, 'An XML part exceeds the entity nesting limit');
44
49
  def(types_1.OfficeCategory.Limit, types_1.OfficeDetail.XmlEntityExpansions, 'reject', 0, 'The file exceeds the entity expansion limit');
45
50
  def(types_1.OfficeCategory.Limit, types_1.OfficeDetail.XmlEntitySize, 'reject', 0, 'The file exceeds the expanded entity size limit');
51
+ def(types_1.OfficeCategory.Limit, types_1.OfficeDetail.RtfDepth, 'reject', 0, 'The RTF file exceeds the group nesting limit');
46
52
  def(types_1.OfficeCategory.Limit, types_1.OfficeDetail.Time, 'reject', 0, 'The file exceeds the time limit');
47
53
  // PACKAGE
48
- def(types_1.OfficeCategory.Package, types_1.OfficeDetail.Unrecognized, 'reject', 0, 'The file is not a recognized Office or OpenDocument package');
54
+ def(types_1.OfficeCategory.Package, types_1.OfficeDetail.Unrecognized, 'reject', 0, 'The file is not a recognized Office or OpenDocument file');
49
55
  def(types_1.OfficeCategory.Package, types_1.OfficeDetail.AmbiguousFormat, 'reject', 0, 'The file can be read as more than one format');
50
56
  def(types_1.OfficeCategory.Package, types_1.OfficeDetail.Unreadable, 'reject', 0, 'The file could not be read');
51
57
  def(types_1.OfficeCategory.Package, types_1.OfficeDetail.DuplicateEntry, 'reject', 0, 'The package names one thing twice, and readers may pick either');
@@ -54,9 +60,19 @@ def(types_1.OfficeCategory.Package, types_1.OfficeDetail.UnreferencedPart, 'stri
54
60
  def(types_1.OfficeCategory.Package, types_1.OfficeDetail.OdfLayout, 'strip', 10, 'The OpenDocument package layout is irregular<: {reason}|>');
55
61
  // ENCRYPTED
56
62
  def(types_1.OfficeCategory.Encrypted, types_1.OfficeDetail.PasswordRequired, 'reject', 0, 'The document is encrypted with a password');
63
+ def(types_1.OfficeCategory.Encrypted, types_1.OfficeDetail.DefaultPassword, 'strip', 10, 'The file is encrypted with VelvetSweatshop, the password Excel opens it with without asking');
64
+ def(types_1.OfficeCategory.Encrypted, types_1.OfficeDetail.UserPassword, 'strip', 0, 'The file was decrypted with the supplied password');
65
+ def(types_1.OfficeCategory.Encrypted, types_1.OfficeDetail.Rc4_40, 'info', 0, 'Encryption is RC4 with a 40-bit key');
66
+ def(types_1.OfficeCategory.Encrypted, types_1.OfficeDetail.Rc4_128, 'info', 0, 'Encryption is RC4 with a 128-bit key');
67
+ def(types_1.OfficeCategory.Encrypted, types_1.OfficeDetail.Rc4Other, 'info', 0, 'Encryption is RC4 with a <{bits}|longer>-bit key');
68
+ def(types_1.OfficeCategory.Encrypted, types_1.OfficeDetail.Aes128, 'info', 0, 'Encryption is AES with a 128-bit key');
69
+ def(types_1.OfficeCategory.Encrypted, types_1.OfficeDetail.Aes192, 'info', 0, 'Encryption is AES with a 192-bit key');
70
+ def(types_1.OfficeCategory.Encrypted, types_1.OfficeDetail.Aes256, 'info', 0, 'Encryption is AES with a 256-bit key');
71
+ def(types_1.OfficeCategory.Encrypted, types_1.OfficeDetail.UnsupportedAlgorithm, 'reject', 0, 'The file is encrypted with an algorithm office-defuse does not decrypt<: {algorithm}|>');
57
72
  // MACRO
58
73
  def(types_1.OfficeCategory.Macro, types_1.OfficeDetail.VbaProject, 'strip', 70, 'The document holds VBA macros');
59
74
  def(types_1.OfficeCategory.Macro, types_1.OfficeDetail.XlmMacrosheet, 'strip', 70, 'The workbook holds an Excel 4.0 macro sheet');
75
+ def(types_1.OfficeCategory.Macro, types_1.OfficeDetail.WordBasic, 'strip', 70, 'The Word 6.0 or 95 document holds WordBasic macros, which Word runs as VBA');
60
76
  def(types_1.OfficeCategory.Macro, types_1.OfficeDetail.OdfBasic, 'strip', 70, 'The document holds Basic macros');
61
77
  def(types_1.OfficeCategory.Macro, types_1.OfficeDetail.OdfScripts, 'strip', 70, 'The document holds scripts');
62
78
  def(types_1.OfficeCategory.Macro, types_1.OfficeDetail.OdfDialogs, 'strip', 30, 'The document holds Basic dialogs, which call macros');
@@ -70,6 +86,7 @@ def(types_1.OfficeCategory.Ui, types_1.OfficeDetail.OdfConfiguration, 'strip', 1
70
86
  // ACTION
71
87
  def(types_1.OfficeCategory.Action, types_1.OfficeDetail.RunProgram, 'strip', 80, 'A click or hover runs a program or opens a file');
72
88
  def(types_1.OfficeCategory.Action, types_1.OfficeDetail.OleVerb, 'strip', 40, 'A click or hover activates an embedded object');
89
+ def(types_1.OfficeCategory.Action, types_1.OfficeDetail.ObjectUpdate, 'strip', 50, 'An embedded object updates itself as the document opens, which starts the program it belongs to');
73
90
  // DDE
74
91
  def(types_1.OfficeCategory.Dde, types_1.OfficeDetail.Field, 'strip', 70, 'A field runs a command through DDE');
75
92
  def(types_1.OfficeCategory.Dde, types_1.OfficeDetail.Link, 'strip', 70, 'A DDE link asks another program for data<: {application}|>');
@@ -100,11 +117,13 @@ def(types_1.OfficeCategory.Link, types_1.OfficeDetail.LookalikeHost, 'strip', 40
100
117
  def(types_1.OfficeCategory.Link, types_1.OfficeDetail.EncodedHost, 'strip', 40, "A link's host is percent-encoded");
101
118
  def(types_1.OfficeCategory.Link, types_1.OfficeDetail.Relative, 'strip', 30, "A relative link, which resolves against the file's own location");
102
119
  // EMBEDDED
103
- def(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.OleObject, 'strip', 40, 'An embedded OLE object');
120
+ def(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.OleObject, 'strip', 40, 'An embedded OLE object<, which holds the file {fileName}|>');
104
121
  def(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Package, 'strip', 30, 'An embedded document<: {contentType}|>');
105
122
  def(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.OleFile, 'strip', 40, 'A part holds an OLE file that nothing declares as an object');
106
123
  def(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Applet, 'strip', 60, 'An embedded Java applet');
107
124
  def(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Plugin, 'strip', 50, 'An embedded browser plug-in object');
125
+ def(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Equation, 'strip', 70, 'An Equation Editor 3.0 object, which CVE-2017-11882 and CVE-2018-0802 exploit');
126
+ def(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Attachment, 'strip', 30, '<An attached file "{name}"|An attached file> (<{type}|unknown type>)');
108
127
  // CONTROL
109
128
  def(types_1.OfficeCategory.Control, types_1.OfficeDetail.ActiveX, 'strip', 50, 'An ActiveX control');
110
129
  /** The built-in spec for a kind of finding, or undefined for a kind office-defuse does not make, such as one from another package. */
package/dist/msg.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ import { type CfbEntry, type CompoundFile } from './cfb';
2
+ import { type RunContext } from './context';
3
+ /**
4
+ * Inspects an Outlook message (MS-OXMSG): each attachment is reported with its name and type, and an attachment by
5
+ * reference with what it points to. An embedded message's own attachments are read as well.
6
+ */
7
+ export declare function messageRules(ctx: RunContext, cfb: CompoundFile, message: CfbEntry, depth?: number): Promise<void>;
package/dist/msg.js ADDED
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.messageRules = messageRules;
4
+ const cfb_1 = require("./cfb");
5
+ const context_1 = require("./context");
6
+ const oleobj_1 = require("./oleobj");
7
+ const types_1 = require("./types");
8
+ /** Property ids of an attachment (MS-OXPROPS). */
9
+ const P = { method: 0x3705, longName: 0x3707, name: 0x3704, display: 0x3001, mime: 0x370e, data: 0x3701, longPath: 0x370d, path: 0x3708 };
10
+ /** How an attachment holds its file (MS-OXCMSG 2.2.2.9): by value, by reference, embedded message, OLE object, web reference. */
11
+ const METHODS = { 1: 'by value', 2: 'by reference', 3: 'by reference, resolved', 4: 'by reference only', 5: 'embedded message', 6: 'OLE object', 7: 'by web reference' };
12
+ /** Embedded messages can hold messages in turn. Past this depth they are still reported, and not read further. */
13
+ const MAX_DEPTH = 16;
14
+ /**
15
+ * Inspects an Outlook message (MS-OXMSG): each attachment is reported with its name and type, and an attachment by
16
+ * reference with what it points to. An embedded message's own attachments are read as well.
17
+ */
18
+ async function messageRules(ctx, cfb, message, depth = 0) {
19
+ for (const a of message.children) {
20
+ if (a.kind !== 'storage' || !/^__attach_version1\.0_#[0-9A-F]{8}$/i.test(a.name))
21
+ continue;
22
+ await attachmentRules(ctx, cfb, a, depth);
23
+ }
24
+ }
25
+ async function attachmentRules(ctx, cfb, a, depth) {
26
+ const location = (0, cfb_1.entryPath)(a);
27
+ const props = await properties(cfb, a);
28
+ const text = async (id) => {
29
+ for (const [type, decode] of [
30
+ ['001F', oleobj_1.utf16],
31
+ ['001E', oleobj_1.latin1],
32
+ ]) {
33
+ const s = a.children.find(c => c.kind === 'stream' && c.name.toUpperCase() === `__SUBSTG1.0_${id.toString(16).toUpperCase().padStart(4, '0')}${type}`);
34
+ if (s && s.size <= 65536)
35
+ return decode(await cfb.bytes(s)).replace(/\0+$/, '');
36
+ }
37
+ return '';
38
+ };
39
+ const method = props.get(P.method) ?? 0;
40
+ const name = (await text(P.longName)) || (await text(P.name)) || (await text(P.display));
41
+ const declaredType = await text(P.mime);
42
+ const dataId = `__SUBSTG1.0_${P.data.toString(16).toUpperCase()}`;
43
+ const data = a.children.find(c => c.name.toUpperCase().startsWith(dataId));
44
+ const info = { attachment: location, method: METHODS[method] ?? String(method) };
45
+ if (name)
46
+ info.name = (0, context_1.clip)(name);
47
+ if (declaredType)
48
+ info.type = (0, context_1.clip)(declaredType);
49
+ if (method >= 2 && method <= 4) {
50
+ const path = (await text(P.longPath)) || (await text(P.path));
51
+ if (path)
52
+ ctx.external(types_1.OfficeDetail.Other, path, location, { source: 'attachment by reference', ...(name ? { name: (0, context_1.clip)(name) } : {}) }, 'attachment');
53
+ return;
54
+ }
55
+ if (method === 7) {
56
+ const path = (await text(P.longPath)) || (await text(P.path));
57
+ if (path)
58
+ ctx.external(types_1.OfficeDetail.Other, path, location, { source: 'attachment by web reference', ...(name ? { name: (0, context_1.clip)(name) } : {}) }, 'attachment');
59
+ return;
60
+ }
61
+ if (!data)
62
+ return;
63
+ if (data.kind === 'stream') {
64
+ attachment(ctx, { name: name || undefined, declaredType: declaredType || undefined, size: data.size, location, depth: 1, source: cfb.source(data) }, info);
65
+ return;
66
+ }
67
+ // An embedded message or an OLE object is a storage of this file. The object rules read an object's storage, and
68
+ // an embedded message's attachments are read here.
69
+ info.size = 0;
70
+ if (method === 5)
71
+ info.type ??= 'application/vnd.ms-outlook';
72
+ ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Attachment, location, info);
73
+ if (method === 5 && depth < MAX_DEPTH)
74
+ await messageRules(ctx, cfb, data, depth + 1);
75
+ }
76
+ /**
77
+ * The place every attached file passes, with what a contained file plugin needs to read it. office-defuse reports it;
78
+ * the family's router, once office-defuse has a plugin for it, is where it would be handed on.
79
+ */
80
+ function attachment(ctx, file, info) {
81
+ ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Attachment, file.location, { ...info, size: file.size });
82
+ }
83
+ /** The fixed-size values of a property stream (MS-OXMSG 2.4.2), by property id. An attachment's has an 8-byte header. */
84
+ async function properties(cfb, storage) {
85
+ const out = new Map();
86
+ const s = storage.children.find(c => c.kind === 'stream' && c.name.toUpperCase() === '__PROPERTIES_VERSION1.0');
87
+ if (!s || s.size > 65536)
88
+ return out;
89
+ const b = await cfb.bytes(s);
90
+ for (let at = 8; at + 16 <= b.length; at += 16) {
91
+ const tag = (b[at] | (b[at + 1] << 8) | (b[at + 2] << 16) | (b[at + 3] << 24)) >>> 0;
92
+ // PT_LONG values sit in the first four bytes of the value.
93
+ if ((tag & 0xffff) === 0x0003)
94
+ out.set(tag >>> 16, (b[at + 8] | (b[at + 9] << 8) | (b[at + 10] << 16) | (b[at + 11] << 24)) >>> 0);
95
+ }
96
+ return out;
97
+ }
@@ -0,0 +1,42 @@
1
+ import { type CfbEntry, type CompoundFile } from './cfb';
2
+ import { type RunContext } from './context';
3
+ import { type PackagedFile } from './oleobj';
4
+ /** Equation Editor 3.0's ProgID, which rtfobj matches as a prefix. */
5
+ export declare const isEquationClass: (name: string) => boolean;
6
+ /** Reports what the reader of a compound file inside this one noted, at the place it sits. */
7
+ export declare function nestedNotes(ctx: RunContext, cfb: CompoundFile, location: string): void;
8
+ /**
9
+ * The rules every compound file gets: VBA projects, embedded objects and controls, wherever they sit in the tree.
10
+ * `prefix` names where the file sits when it is inside another, and `rootIsObject` says that its root is itself an
11
+ * OLE object's storage, as an object in a presentation or an RTF file is.
12
+ */
13
+ export declare function compoundRules(ctx: RunContext, cfb: CompoundFile, prefix: string, rootIsObject: boolean): Promise<void>;
14
+ /** What an OLE object's storage says about the object. */
15
+ export interface ObjectInfo {
16
+ clsid: string;
17
+ progId?: string;
18
+ userType?: string;
19
+ /** An ActiveX control's storage, with the control's name. */
20
+ control: boolean;
21
+ controlName?: string;
22
+ linked: boolean;
23
+ /** What a linked object loads, or ''. */
24
+ target: string;
25
+ /** The target is a URL moniker's, which loads from outside however it is spelled. */
26
+ url: boolean;
27
+ /** A Packager object's file, held or linked to. */
28
+ pkg?: PackagedFile;
29
+ equation: boolean;
30
+ }
31
+ /** Reads an OLE object's storage: its class, its CompObj and OLE streams, and Packager's native data. */
32
+ export declare function describeObject(cfb: CompoundFile, e: CfbEntry, _location: string): Promise<ObjectInfo>;
33
+ /**
34
+ * Reports a linked object. A URL moniker's target loads from outside the file whatever its text looks like: a
35
+ * CVE-2017-0199 sample puts a character with a stray high byte where the colon of https: goes.
36
+ */
37
+ export declare function linkedObject(ctx: RunContext, d: {
38
+ target: string;
39
+ url: boolean;
40
+ }, location: string, data: Record<string, string>): void;
41
+ /** What a finding shows of a Packager object's file: its name and paths, and its size when the object holds it. */
42
+ export declare function packageData(p: PackagedFile): Record<string, string | number>;