@patrickdk77/office-defuse 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +211 -43
- package/dist/cfb-writer.d.ts +25 -0
- package/dist/cfb-writer.js +314 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +357 -0
- package/dist/contained.d.ts +46 -0
- package/dist/contained.js +152 -0
- package/dist/context.d.ts +107 -12
- package/dist/context.js +101 -24
- package/dist/detect.d.ts +13 -0
- package/dist/detect.js +83 -0
- package/dist/engine.d.ts +66 -1
- package/dist/engine.js +290 -35
- package/dist/errors.d.ts +17 -0
- package/dist/errors.js +27 -1
- package/dist/fields.d.ts +20 -13
- package/dist/fields.js +65 -23
- package/dist/findings.d.ts +2 -1
- package/dist/findings.js +17 -1
- package/dist/flat.d.ts +33 -0
- package/dist/flat.js +555 -0
- package/dist/formats.d.ts +9 -0
- package/dist/formats.js +7 -1
- package/dist/formulas.d.ts +27 -0
- package/dist/formulas.js +157 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +11 -1
- package/dist/index.mjs +1 -1
- package/dist/io.d.ts +62 -1
- package/dist/io.js +245 -1
- package/dist/links.d.ts +27 -0
- package/dist/links.js +106 -1
- package/dist/msg.d.ts +3 -2
- package/dist/msg.js +172 -18
- package/dist/objects.d.ts +8 -5
- package/dist/objects.js +90 -20
- package/dist/odf.d.ts +87 -1
- package/dist/odf.js +204 -76
- package/dist/offcrypto.d.ts +24 -1
- package/dist/offcrypto.js +308 -69
- package/dist/ole.d.ts +19 -2
- package/dist/ole.js +98 -58
- package/dist/oleobj.d.ts +11 -0
- package/dist/oleobj.js +20 -3
- package/dist/ooxml.d.ts +108 -2
- package/dist/ooxml.js +392 -109
- package/dist/package.d.ts +14 -0
- package/dist/package.js +23 -7
- package/dist/plan.d.ts +104 -0
- package/dist/plan.js +73 -0
- package/dist/plugins.d.ts +8 -0
- package/dist/plugins.js +35 -0
- package/dist/ppt.js +45 -5
- package/dist/rewrite-cfb.d.ts +14 -0
- package/dist/rewrite-cfb.js +52 -0
- package/dist/rewrite-rtf.d.ts +9 -0
- package/dist/rewrite-rtf.js +45 -0
- package/dist/rewrite-xml.d.ts +36 -0
- package/dist/rewrite-xml.js +341 -0
- package/dist/rewrite-zip.d.ts +8 -0
- package/dist/rewrite-zip.js +498 -0
- package/dist/rtf.js +216 -42
- package/dist/rtfcomp.d.ts +7 -0
- package/dist/rtfcomp.js +80 -0
- package/dist/score.js +1 -1
- package/dist/sniff.d.ts +5 -0
- package/dist/sniff.js +38 -0
- package/dist/types.d.ts +66 -6
- package/dist/types.js +14 -0
- package/dist/vsd.d.ts +13 -0
- package/dist/vsd.js +689 -0
- package/dist/word.d.ts +4 -4
- package/dist/word.js +70 -23
- package/dist/xls.d.ts +10 -4
- package/dist/xls.js +58 -10
- package/dist/xml.d.ts +30 -0
- package/dist/xml.js +45 -23
- package/dist/zip-writer.d.ts +4 -0
- package/dist/zip-writer.js +20 -7
- package/dist/zip.d.ts +11 -2
- package/dist/zip.js +67 -6
- package/package.json +4 -1
package/dist/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
|
-
|
|
154
|
-
|
|
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
|
-
/**
|
|
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('
|
|
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
|
-
|
|
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.
|
|
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",
|