@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.
Files changed (82) hide show
  1. package/README.md +211 -43
  2. package/dist/cfb-writer.d.ts +25 -0
  3. package/dist/cfb-writer.js +314 -0
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +357 -0
  6. package/dist/contained.d.ts +46 -0
  7. package/dist/contained.js +152 -0
  8. package/dist/context.d.ts +107 -12
  9. package/dist/context.js +101 -24
  10. package/dist/detect.d.ts +13 -0
  11. package/dist/detect.js +83 -0
  12. package/dist/engine.d.ts +66 -1
  13. package/dist/engine.js +290 -35
  14. package/dist/errors.d.ts +17 -0
  15. package/dist/errors.js +27 -1
  16. package/dist/fields.d.ts +20 -13
  17. package/dist/fields.js +65 -23
  18. package/dist/findings.d.ts +2 -1
  19. package/dist/findings.js +17 -1
  20. package/dist/flat.d.ts +33 -0
  21. package/dist/flat.js +555 -0
  22. package/dist/formats.d.ts +9 -0
  23. package/dist/formats.js +7 -1
  24. package/dist/formulas.d.ts +27 -0
  25. package/dist/formulas.js +157 -1
  26. package/dist/index.d.ts +4 -2
  27. package/dist/index.js +11 -1
  28. package/dist/index.mjs +1 -1
  29. package/dist/io.d.ts +62 -1
  30. package/dist/io.js +245 -1
  31. package/dist/links.d.ts +27 -0
  32. package/dist/links.js +106 -1
  33. package/dist/msg.d.ts +3 -2
  34. package/dist/msg.js +172 -18
  35. package/dist/objects.d.ts +8 -5
  36. package/dist/objects.js +90 -20
  37. package/dist/odf.d.ts +87 -1
  38. package/dist/odf.js +204 -76
  39. package/dist/offcrypto.d.ts +24 -1
  40. package/dist/offcrypto.js +308 -69
  41. package/dist/ole.d.ts +19 -2
  42. package/dist/ole.js +98 -58
  43. package/dist/oleobj.d.ts +11 -0
  44. package/dist/oleobj.js +20 -3
  45. package/dist/ooxml.d.ts +108 -2
  46. package/dist/ooxml.js +392 -109
  47. package/dist/package.d.ts +14 -0
  48. package/dist/package.js +23 -7
  49. package/dist/plan.d.ts +104 -0
  50. package/dist/plan.js +73 -0
  51. package/dist/plugins.d.ts +8 -0
  52. package/dist/plugins.js +35 -0
  53. package/dist/ppt.js +45 -5
  54. package/dist/rewrite-cfb.d.ts +14 -0
  55. package/dist/rewrite-cfb.js +52 -0
  56. package/dist/rewrite-rtf.d.ts +9 -0
  57. package/dist/rewrite-rtf.js +45 -0
  58. package/dist/rewrite-xml.d.ts +36 -0
  59. package/dist/rewrite-xml.js +341 -0
  60. package/dist/rewrite-zip.d.ts +8 -0
  61. package/dist/rewrite-zip.js +498 -0
  62. package/dist/rtf.js +216 -42
  63. package/dist/rtfcomp.d.ts +7 -0
  64. package/dist/rtfcomp.js +80 -0
  65. package/dist/score.js +1 -1
  66. package/dist/sniff.d.ts +5 -0
  67. package/dist/sniff.js +38 -0
  68. package/dist/types.d.ts +66 -6
  69. package/dist/types.js +14 -0
  70. package/dist/vsd.d.ts +13 -0
  71. package/dist/vsd.js +689 -0
  72. package/dist/word.d.ts +4 -4
  73. package/dist/word.js +70 -23
  74. package/dist/xls.d.ts +10 -4
  75. package/dist/xls.js +58 -10
  76. package/dist/xml.d.ts +30 -0
  77. package/dist/xml.js +45 -23
  78. package/dist/zip-writer.d.ts +4 -0
  79. package/dist/zip-writer.js +20 -7
  80. package/dist/zip.d.ts +11 -2
  81. package/dist/zip.js +67 -6
  82. package/package.json +4 -1
package/dist/zip.js CHANGED
@@ -56,6 +56,22 @@ const END_SEARCH = 22 + MAX16;
56
56
  const FLAG_MASK = 0x0001 | 0x0008 | 0x0040 | 0x0800 | 0x2000;
57
57
  const ENCRYPTION_FLAGS = 0x0001 | 0x0040 | 0x2000;
58
58
  const MAX_NAME_BYTES = 1024;
59
+ const LOCAL_SIGNATURE = Buffer.from([0x50, 0x4b, 0x03, 0x04]);
60
+ /** The offset of the first local file header signature in [start, end) of the file, or undefined. */
61
+ async function findLocalHeader(reader, start, end, deadline) {
62
+ let carry = Buffer.alloc(0);
63
+ let at = start;
64
+ for await (const c of reader.chunks(start, end - start)) {
65
+ (0, errors_1.checkTime)(deadline);
66
+ const b = Buffer.concat([carry, Buffer.from(c.buffer, c.byteOffset, c.byteLength)]);
67
+ const i = b.indexOf(LOCAL_SIGNATURE);
68
+ if (i >= 0)
69
+ return at - carry.length + i;
70
+ carry = b.subarray(Math.max(0, b.length - 3));
71
+ at += c.length;
72
+ }
73
+ return undefined;
74
+ }
59
75
  const fail = (detail, message, data) => {
60
76
  throw new errors_1.StopError(types_1.OfficeCategory.Container, detail, message, data);
61
77
  };
@@ -137,6 +153,20 @@ function checkUnicodePath(extras, name) {
137
153
  if (other !== name)
138
154
  fail(types_1.OfficeDetail.NameEncoding, 'The Unicode path field names a different entry', { name });
139
155
  }
156
+ /** True for a segment of nothing but dots and spaces, the empty one included. */
157
+ function onlyDotsAndSpaces(seg) {
158
+ for (const c of seg)
159
+ if (c !== '.' && c !== ' ')
160
+ return false;
161
+ return true;
162
+ }
163
+ /** A segment without the dots and spaces Windows drops from its end. A loop, since /[. ]+$/ backtracks. */
164
+ function trimSegment(seg) {
165
+ let end = seg.length;
166
+ while (end > 0 && (seg[end - 1] === '.' || seg[end - 1] === ' '))
167
+ end--;
168
+ return seg.slice(0, end);
169
+ }
140
170
  function checkName(name, directory) {
141
171
  const why = (reason) => fail(types_1.OfficeDetail.UnsafeName, `Entry name ${reason}`, { name, reason });
142
172
  if (name === '')
@@ -150,11 +180,17 @@ function checkName(name, directory) {
150
180
  if (/^[A-Za-z]:/.test(name))
151
181
  why('starts with a drive letter');
152
182
  for (const seg of (directory ? name.slice(0, -1) : name).split('/')) {
153
- if (seg === '' || seg === '.' || seg === '..')
154
- why('has an empty, "." or ".." segment');
183
+ // Windows drops trailing dots and spaces from each segment, so ".. " and "..." climb out as ".." does.
184
+ if (onlyDotsAndSpaces(seg))
185
+ why('has an empty segment, or one of only dots and spaces');
155
186
  }
156
187
  }
157
- /** The key two names collide on: percent-decoded, NFC and lowercase, without a directory's slash. */
188
+ /**
189
+ * The key two names collide on: percent-decoded, NFKC and case-folded, without a directory's slash, and without the
190
+ * dots and spaces Windows drops from the end of each segment. Upper then lower case is the full folding, which takes a
191
+ * sharp s to ss as macOS's case-insensitive file system does. NFKC also joins compatibility forms such as ligatures
192
+ * and full-width letters.
193
+ */
158
194
  function nameKey(name) {
159
195
  let n = name;
160
196
  try {
@@ -163,7 +199,7 @@ function nameKey(name) {
163
199
  catch {
164
200
  // Not valid percent-encoding, so it stays as written.
165
201
  }
166
- return n.normalize('NFC').toLowerCase().replace(/\/$/, '');
202
+ return n.normalize('NFKC').toUpperCase().toLowerCase().replace(/\/$/, '').split('/').map(trimSegment).join('/');
167
203
  }
168
204
  /** ZIP64 values the central directory defers to its extra field, in the order the format fixes. */
169
205
  function zip64Central(extras, fields, name) {
@@ -205,12 +241,16 @@ class ZipArchive {
205
241
  entries;
206
242
  notes;
207
243
  deadline;
244
+ start;
208
245
  byKey = new Map();
209
- 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) {
210
249
  this.reader = reader;
211
250
  this.entries = entries;
212
251
  this.notes = notes;
213
252
  this.deadline = deadline;
253
+ this.start = start;
214
254
  for (const e of entries)
215
255
  this.byKey.set(nameKey(e.name), e);
216
256
  }
@@ -371,26 +411,47 @@ class ZipArchive {
371
411
  (0, errors_1.checkTime)(deadline);
372
412
  await ZipArchive.checkLocal(reader, e, raws[i], dirOffset);
373
413
  }
414
+ // A reader that walks the file cannot know where a stored entry with a data descriptor ends. It looks for the
415
+ // next signature, so a local header inside the data is an entry to it, and part of this entry here.
416
+ for (const e of entries) {
417
+ if (e.method !== 0 || !(e.flags & 0x0008) || e.compressedSize === 0)
418
+ continue;
419
+ const at = await findLocalHeader(reader, e.dataOffset, e.dataOffset + e.compressedSize, deadline);
420
+ if (at !== undefined)
421
+ notes.push({ detail: types_1.OfficeDetail.HiddenEntry, bytes: e.dataOffset + e.compressedSize - at, offset: at });
422
+ }
374
423
  // Entries and the central directory must tile the file. Bytes no entry covers are reported, overlaps refused.
375
424
  const sorted = [...entries].sort((a, b) => a.localOffset - b.localOffset);
425
+ const gaps = [];
376
426
  let covered = 0;
377
427
  let hidden = 0;
378
428
  for (const e of sorted) {
379
429
  if (e.localOffset < covered)
380
430
  fail(types_1.OfficeDetail.Overlap, 'Two entries share bytes', { name: e.name });
431
+ if (e.localOffset > covered)
432
+ gaps.push([covered, e.localOffset]);
381
433
  if (covered === 0 && e.localOffset > 0)
382
434
  notes.push({ detail: types_1.OfficeDetail.LeadingBytes, bytes: e.localOffset });
383
435
  else
384
436
  hidden += e.localOffset - covered;
385
437
  covered = e.end;
386
438
  }
439
+ if (dirOffset > covered)
440
+ gaps.push([covered, dirOffset]);
387
441
  if (sorted.length === 0 && dirOffset > 0)
388
442
  notes.push({ detail: types_1.OfficeDetail.LeadingBytes, bytes: dirOffset });
389
443
  else
390
444
  hidden += dirOffset - covered;
391
445
  if (hidden > 0)
392
446
  notes.push({ detail: types_1.OfficeDetail.HiddenData, bytes: hidden });
393
- return new ZipArchive(reader, entries, notes, deadline);
447
+ // A local header in a gap is an entry the central directory does not list. A reader that walks the file from the
448
+ // start, as streaming unzippers do, extracts it; a reader of the central directory never sees it.
449
+ for (const [from, to] of gaps) {
450
+ const at = await findLocalHeader(reader, from, to, deadline);
451
+ if (at !== undefined)
452
+ notes.push({ detail: types_1.OfficeDetail.HiddenEntry, bytes: to - from, offset: at });
453
+ }
454
+ return new ZipArchive(reader, entries, notes, deadline, sorted.length ? sorted[0].localOffset : dirOffset);
394
455
  }
395
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. */
396
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.1.1",
3
+ "version": "0.2.1",
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",
@@ -16,6 +16,9 @@
16
16
  },
17
17
  "./package.json": "./package.json"
18
18
  },
19
+ "bin": {
20
+ "office-defuse": "dist/cli.js"
21
+ },
19
22
  "files": [
20
23
  "dist",
21
24
  "README.md",