@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/README.md +113 -26
- package/dist/biff.d.ts +62 -0
- package/dist/biff.js +95 -0
- package/dist/cfb.d.ts +58 -0
- package/dist/cfb.js +456 -0
- package/dist/context.d.ts +40 -12
- package/dist/context.js +46 -25
- package/dist/crc32.d.ts +8 -2
- package/dist/crc32.js +44 -4
- package/dist/engine.js +75 -26
- package/dist/fields.d.ts +29 -0
- package/dist/fields.js +97 -0
- package/dist/findings.js +28 -9
- package/dist/msg.d.ts +7 -0
- package/dist/msg.js +97 -0
- package/dist/objects.d.ts +42 -0
- package/dist/objects.js +134 -0
- package/dist/odf.d.ts +2 -2
- package/dist/offcrypto.d.ts +31 -0
- package/dist/offcrypto.js +408 -0
- package/dist/ole.d.ts +12 -0
- package/dist/ole.js +127 -0
- package/dist/oleobj.d.ts +74 -0
- package/dist/oleobj.js +198 -0
- package/dist/ooxml.d.ts +2 -2
- package/dist/ooxml.js +23 -71
- package/dist/package.d.ts +3 -0
- package/dist/package.js +21 -0
- package/dist/ppt.d.ts +8 -0
- package/dist/ppt.js +241 -0
- package/dist/rtf.d.ts +8 -0
- package/dist/rtf.js +539 -0
- package/dist/types.d.ts +27 -0
- package/dist/types.js +19 -0
- package/dist/word.d.ts +9 -0
- package/dist/word.js +160 -0
- package/dist/xls.d.ts +27 -0
- package/dist/xls.js +174 -0
- package/package.json +3 -3
package/dist/word.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.wordRules = wordRules;
|
|
4
|
+
const errors_1 = require("./errors");
|
|
5
|
+
const fields_1 = require("./fields");
|
|
6
|
+
const io_1 = require("./io");
|
|
7
|
+
const oleobj_1 = require("./oleobj");
|
|
8
|
+
const types_1 = require("./types");
|
|
9
|
+
/** wIdent of Word 97 and later, and of Word 6.0 and 95 (MS-DOC 2.5.2). */
|
|
10
|
+
const WORD8 = 0xa5ec;
|
|
11
|
+
const WORD6 = 0xa5dc;
|
|
12
|
+
/** FibRgFcLcb97 pairs (MS-DOC 2.5.6): the attached template's table and the piece table. */
|
|
13
|
+
const FC_STTBF_ASSOC = 32;
|
|
14
|
+
const FC_CLX = 33;
|
|
15
|
+
/** In a Word 6.0 or 95 FIB, the length of the macro table that holds WordBasic macros. Word writes 2 for an empty one. */
|
|
16
|
+
const WORD6_MACROS = 0x11c;
|
|
17
|
+
/** Text is read in pieces of at most this many characters. */
|
|
18
|
+
const PIECE = 65536;
|
|
19
|
+
const u16 = (b, at) => b[at] | (b[at + 1] << 8);
|
|
20
|
+
const u32 = (b, at) => (b[at] | (b[at + 1] << 8) | (b[at + 2] << 16) | (b[at + 3] << 24)) >>> 0;
|
|
21
|
+
/**
|
|
22
|
+
* Inspects a Word 6.0 to 2003 document: its FIB, whether it is encrypted, the fields in its text and the template it
|
|
23
|
+
* attaches. Returns whether it is a template, or undefined for an `optional` stream that holds no FIB.
|
|
24
|
+
*/
|
|
25
|
+
async function wordRules(ctx, cfb, stream, optional = false) {
|
|
26
|
+
const source = cfb.source(stream);
|
|
27
|
+
const fib = await source.read(0, 1024);
|
|
28
|
+
const ident = fib.length >= 32 ? u16(fib, 0) : 0;
|
|
29
|
+
if (ident !== WORD8 && ident !== WORD6) {
|
|
30
|
+
if (optional)
|
|
31
|
+
return undefined;
|
|
32
|
+
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' });
|
|
33
|
+
}
|
|
34
|
+
const flags = u16(fib, 10);
|
|
35
|
+
// fEncrypted, set for both RC4 encryption and XOR obfuscation.
|
|
36
|
+
if (flags & 0x0100)
|
|
37
|
+
throw new errors_1.StopError(types_1.OfficeCategory.Encrypted, types_1.OfficeDetail.PasswordRequired, 'The document is encrypted', { stream: stream.name });
|
|
38
|
+
const location = stream.name;
|
|
39
|
+
const fields = new fields_1.FieldStack(ctx, location);
|
|
40
|
+
// Word marks a field's start, separator and end with 0x13, 0x14 and 0x15 in the text. Other control characters in
|
|
41
|
+
// a field's code, such as the 0x01 of a hyperlink's data, are not part of what it says.
|
|
42
|
+
const text = (s) => {
|
|
43
|
+
for (const ch of s) {
|
|
44
|
+
if (ch === '\x13')
|
|
45
|
+
fields.begin();
|
|
46
|
+
else if (ch === '\x14')
|
|
47
|
+
fields.separate();
|
|
48
|
+
else if (ch === '\x15')
|
|
49
|
+
fields.end();
|
|
50
|
+
else if (fields.inCode && (ch >= ' ' || ch === '\t'))
|
|
51
|
+
fields.text(ch);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const reader = new io_1.Reader(source, stream.size, 65536, 8);
|
|
55
|
+
if (ident === WORD6) {
|
|
56
|
+
// Word 6.0 and 95 keep their text as 8-bit characters from fcMin to fcMac. A file saved fast adds pieces to the
|
|
57
|
+
// end, and this reads every byte between, as msodde does.
|
|
58
|
+
const from = u32(fib, 0x18);
|
|
59
|
+
const to = Math.min(u32(fib, 0x1c), stream.size);
|
|
60
|
+
for (let at = from; at < to; at += PIECE) {
|
|
61
|
+
(0, errors_1.checkTime)(ctx.settings.deadline);
|
|
62
|
+
text(latin(await reader.read(at, Math.min(PIECE, to - at))));
|
|
63
|
+
}
|
|
64
|
+
if (fib.length >= WORD6_MACROS + 4 && u32(fib, WORD6_MACROS) > 2)
|
|
65
|
+
ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.WordBasic, location, { stream: location, bytes: u32(fib, WORD6_MACROS) });
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
const pairs = fcLcb(fib);
|
|
69
|
+
const table = cfb.child(cfb.root, flags & 0x0200 ? '1Table' : '0Table');
|
|
70
|
+
if (table?.kind === 'stream') {
|
|
71
|
+
const tableSource = cfb.source(table);
|
|
72
|
+
const clx = pairs[FC_CLX];
|
|
73
|
+
if (clx && clx.lcb > 0 && clx.fc + clx.lcb <= table.size) {
|
|
74
|
+
for (const p of pieces(await tableSource.read(clx.fc, clx.lcb))) {
|
|
75
|
+
for (let cp = 0; cp < p.length; cp += PIECE) {
|
|
76
|
+
(0, errors_1.checkTime)(ctx.settings.deadline);
|
|
77
|
+
const n = Math.min(PIECE, p.length - cp);
|
|
78
|
+
if (p.compressed)
|
|
79
|
+
text(latin(await reader.read(p.fc + cp, n)));
|
|
80
|
+
else
|
|
81
|
+
text((0, oleobj_1.utf16)(await reader.read(p.fc + cp * 2, n * 2)));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const assoc = pairs[FC_STTBF_ASSOC];
|
|
86
|
+
if (assoc && assoc.lcb > 0 && assoc.fc + assoc.lcb <= table.size) {
|
|
87
|
+
// The attached template is string 1 of SttbfAssoc (MS-DOC 2.9.271).
|
|
88
|
+
const template = sttb(await tableSource.read(assoc.fc, Math.min(assoc.lcb, 65536)))[1] ?? '';
|
|
89
|
+
if (template.trim())
|
|
90
|
+
ctx.external(types_1.OfficeDetail.Template, template, location, { source: 'SttbfAssoc' }, 'template');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
fields.finish();
|
|
95
|
+
return { template: (flags & 1) === 1 };
|
|
96
|
+
}
|
|
97
|
+
/** The fc and lcb pairs of a Word 97 FIB, after its variable-length arrays of 16- and 32-bit values. */
|
|
98
|
+
function fcLcb(fib) {
|
|
99
|
+
const r = new oleobj_1.ByteReader(fib);
|
|
100
|
+
const out = [];
|
|
101
|
+
return (0, oleobj_1.bounded)(() => {
|
|
102
|
+
r.skip(32);
|
|
103
|
+
r.skip(r.u16() * 2);
|
|
104
|
+
r.skip(r.u16() * 4);
|
|
105
|
+
const n = r.u16();
|
|
106
|
+
for (let i = 0; i < n; i++)
|
|
107
|
+
out.push({ fc: r.u32(), lcb: r.u32() });
|
|
108
|
+
return out;
|
|
109
|
+
}, () => out);
|
|
110
|
+
}
|
|
111
|
+
/** The pieces of a Clx (MS-DOC 2.9.38): where each run of text is, and whether it is stored 8 bits a character. */
|
|
112
|
+
function pieces(clx) {
|
|
113
|
+
const r = new oleobj_1.ByteReader(clx);
|
|
114
|
+
const out = [];
|
|
115
|
+
return (0, oleobj_1.bounded)(() => {
|
|
116
|
+
// Prc entries of formatting come first, each with its size.
|
|
117
|
+
while (r.left > 0 && clx[r.pos] === 0x01) {
|
|
118
|
+
r.skip(1);
|
|
119
|
+
r.skip(r.u16());
|
|
120
|
+
}
|
|
121
|
+
if (r.u8() !== 0x02)
|
|
122
|
+
return out;
|
|
123
|
+
const lcb = r.u32();
|
|
124
|
+
const n = Math.floor((lcb - 4) / 12);
|
|
125
|
+
const plc = new oleobj_1.ByteReader(r.bytes(Math.min(lcb, r.left)));
|
|
126
|
+
const cps = [];
|
|
127
|
+
for (let i = 0; i <= n; i++)
|
|
128
|
+
cps.push(plc.u32());
|
|
129
|
+
for (let i = 0; i < n; i++) {
|
|
130
|
+
plc.skip(2);
|
|
131
|
+
const fc = plc.u32();
|
|
132
|
+
plc.skip(2);
|
|
133
|
+
const length = cps[i + 1] - cps[i];
|
|
134
|
+
if (length <= 0)
|
|
135
|
+
continue;
|
|
136
|
+
const compressed = (fc & 0x40000000) !== 0;
|
|
137
|
+
out.push({ fc: compressed ? (fc & 0x3fffffff) / 2 : fc, length, compressed });
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
}, () => []);
|
|
141
|
+
}
|
|
142
|
+
/** The strings of an STTB (MS-DOC 2.2.4), in 16-bit characters when it starts with 0xFFFF, as Word 97 writes one. */
|
|
143
|
+
function sttb(b) {
|
|
144
|
+
const r = new oleobj_1.ByteReader(b);
|
|
145
|
+
const out = [];
|
|
146
|
+
return (0, oleobj_1.bounded)(() => {
|
|
147
|
+
const wide = r.u16() === 0xffff;
|
|
148
|
+
if (!wide)
|
|
149
|
+
r.pos = 0;
|
|
150
|
+
const count = r.u16();
|
|
151
|
+
const extra = r.u16();
|
|
152
|
+
for (let i = 0; i < count; i++) {
|
|
153
|
+
const n = wide ? r.u16() : r.u8();
|
|
154
|
+
out.push(wide ? (0, oleobj_1.utf16)(r.bytes(n * 2)) : latin(r.bytes(n)));
|
|
155
|
+
r.skip(extra);
|
|
156
|
+
}
|
|
157
|
+
return out;
|
|
158
|
+
}, () => out);
|
|
159
|
+
}
|
|
160
|
+
const latin = (b) => Buffer.from(b.buffer, b.byteOffset, b.byteLength).toString('latin1');
|
package/dist/xls.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type BiffRecord } from './biff';
|
|
2
|
+
import type { CfbEntry, CompoundFile } from './cfb';
|
|
3
|
+
import { type RunContext } from './context';
|
|
4
|
+
/** Turns an encrypted record body into its plain bytes. Without encryption the body is as stored. */
|
|
5
|
+
export type Decrypt = (r: BiffRecord) => Uint8Array;
|
|
6
|
+
/**
|
|
7
|
+
* Opens a workbook encrypted by its FILEPASS record, or says it cannot. Set by the decryption module, so this module
|
|
8
|
+
* reads workbooks without knowing how they are encrypted.
|
|
9
|
+
*/
|
|
10
|
+
export type Unlock = (ctx: RunContext, filePass: BiffRecord, stream: string) => Decrypt | Promise<Decrypt>;
|
|
11
|
+
/**
|
|
12
|
+
* Inspects an Excel 5.0 to 2003 workbook stream. Excel reads the globals substream from the start of the stream,
|
|
13
|
+
* then each sheet from where its BoundSheet record says the sheet starts. The globals give the macro sheets, the
|
|
14
|
+
* names Excel runs as macros, and the DDE, OLE and workbook links. The sheets give shapes that run macros and
|
|
15
|
+
* hyperlinks. An encrypted workbook goes through `unlock`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function excelRules(ctx: RunContext, cfb: CompoundFile, stream: CfbEntry, unlock: Unlock): Promise<{
|
|
18
|
+
template: boolean;
|
|
19
|
+
addIn: boolean;
|
|
20
|
+
}>;
|
|
21
|
+
/**
|
|
22
|
+
* A VirtualPath (MS-XLS 2.5.277) written out. One that starts with U+0001 is encoded: its control characters name a
|
|
23
|
+
* volume, the root, a folder, a step up, a URL's volume or Excel's startup folders.
|
|
24
|
+
*/
|
|
25
|
+
export declare function virtualPath(p: string): string;
|
|
26
|
+
/** The target of an HLink record: its Hyperlink Object (MS-OSHARED 2.3.7.1) after the cell range and class id. */
|
|
27
|
+
export declare function hyperlink(b: Uint8Array): string;
|
package/dist/xls.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.excelRules = excelRules;
|
|
4
|
+
exports.virtualPath = virtualPath;
|
|
5
|
+
exports.hyperlink = hyperlink;
|
|
6
|
+
const biff_1 = require("./biff");
|
|
7
|
+
const context_1 = require("./context");
|
|
8
|
+
const errors_1 = require("./errors");
|
|
9
|
+
const links_1 = require("./links");
|
|
10
|
+
const oleobj_1 = require("./oleobj");
|
|
11
|
+
const types_1 = require("./types");
|
|
12
|
+
const STATES = ['visible', 'hidden', 'very hidden', 'hidden'];
|
|
13
|
+
/**
|
|
14
|
+
* Inspects an Excel 5.0 to 2003 workbook stream. Excel reads the globals substream from the start of the stream,
|
|
15
|
+
* then each sheet from where its BoundSheet record says the sheet starts. The globals give the macro sheets, the
|
|
16
|
+
* names Excel runs as macros, and the DDE, OLE and workbook links. The sheets give shapes that run macros and
|
|
17
|
+
* hyperlinks. An encrypted workbook goes through `unlock`.
|
|
18
|
+
*/
|
|
19
|
+
async function excelRules(ctx, cfb, stream, unlock) {
|
|
20
|
+
const location = stream.name;
|
|
21
|
+
const records = new biff_1.RecordReader(cfb, stream, 0, ctx.settings.deadline);
|
|
22
|
+
const first = await records.next();
|
|
23
|
+
if (first?.type !== biff_1.R.bof || first.body.length < 4)
|
|
24
|
+
throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.Unrecognized, `The ${stream.name} stream does not start with a BOF record`, { reason: 'a workbook stream without a BOF record' });
|
|
25
|
+
const biff8 = (first.body[0] | (first.body[1] << 8)) === 0x0600;
|
|
26
|
+
let decrypt = r => r.body;
|
|
27
|
+
const sheets = [];
|
|
28
|
+
const books = [];
|
|
29
|
+
const flags = { template: false, addIn: false };
|
|
30
|
+
for (let r = await records.next(); r && r.type !== biff_1.R.eof; r = await records.next()) {
|
|
31
|
+
if (r.type === biff_1.R.filePass) {
|
|
32
|
+
decrypt = await unlock(ctx, r, location);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const body = decrypt(r);
|
|
36
|
+
if (r.type === biff_1.R.boundSheet)
|
|
37
|
+
sheets.push((0, biff_1.parseBoundSheet)(body, biff8));
|
|
38
|
+
else if (r.type === biff_1.R.template)
|
|
39
|
+
flags.template = true;
|
|
40
|
+
else if (r.type === biff_1.R.addIn)
|
|
41
|
+
flags.addIn = true;
|
|
42
|
+
else if (r.type === biff_1.R.lbl) {
|
|
43
|
+
const name = (0, biff_1.parseLbl)(body, biff8);
|
|
44
|
+
const auto = (0, biff_1.autoName)(name.name);
|
|
45
|
+
if (auto)
|
|
46
|
+
ctx.report(types_1.OfficeCategory.Macro, auto.onOpen ? types_1.OfficeDetail.EventOnOpen : types_1.OfficeDetail.Event, location, { event: auto.event, name: (0, context_1.clip)(name.name) });
|
|
47
|
+
}
|
|
48
|
+
else if (r.type === biff_1.R.supBook && body.length >= 4) {
|
|
49
|
+
const cch = body[2] | (body[3] << 8);
|
|
50
|
+
books.push({ ctab: body[0] | (body[1] << 8), cch, path: cch > 0 && cch <= 0xff ? (0, biff_1.xlString)(body, 4, cch).text : '', ole: false, dde: false });
|
|
51
|
+
}
|
|
52
|
+
else if (r.type === biff_1.R.externName && body.length >= 2 && books.length) {
|
|
53
|
+
// fOle and fOleLink mark an OLE object's name. Excel writes StdDocumentName with fOle beside the items of a DDE
|
|
54
|
+
// link, so a link with any DDE item is a DDE link.
|
|
55
|
+
if ((body[0] | (body[1] << 8)) & 0x18)
|
|
56
|
+
books[books.length - 1].ole = true;
|
|
57
|
+
else
|
|
58
|
+
books[books.length - 1].dde = true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
for (const b of books)
|
|
62
|
+
linkRules(ctx, b, location);
|
|
63
|
+
for (const s of sheets)
|
|
64
|
+
await sheetRules(ctx, cfb, stream, s, decrypt, location);
|
|
65
|
+
return flags;
|
|
66
|
+
}
|
|
67
|
+
/** A SupBook record, read as MS-XLS 2.4.271 and oletools tell its kinds apart. */
|
|
68
|
+
function linkRules(ctx, b, location) {
|
|
69
|
+
if (b.cch === 0x401 || b.cch === 0x3a01 || b.path === ' ' || b.path === '\0' || !b.path)
|
|
70
|
+
return;
|
|
71
|
+
if (b.ctab === 0) {
|
|
72
|
+
// A DDE or OLE data source: the application or class, U+0003, and the topic or file.
|
|
73
|
+
const [application, topic = ''] = b.path.split('\x03');
|
|
74
|
+
if (b.ole && !b.dde) {
|
|
75
|
+
const data = { source: 'SupBook', className: (0, context_1.clip)(application) };
|
|
76
|
+
if (topic)
|
|
77
|
+
ctx.external(types_1.OfficeDetail.Object, topic, location, data, 'linked object');
|
|
78
|
+
else
|
|
79
|
+
ctx.report(types_1.OfficeCategory.External, types_1.OfficeDetail.Object, location, data);
|
|
80
|
+
}
|
|
81
|
+
else
|
|
82
|
+
ctx.report(types_1.OfficeCategory.Dde, types_1.OfficeDetail.Link, location, { application: (0, context_1.clip)(application), topic: (0, context_1.clip)(topic) });
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
ctx.external(types_1.OfficeDetail.Workbook, virtualPath(b.path), location, { source: 'SupBook' }, 'external workbook');
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* A VirtualPath (MS-XLS 2.5.277) written out. One that starts with U+0001 is encoded: its control characters name a
|
|
89
|
+
* volume, the root, a folder, a step up, a URL's volume or Excel's startup folders.
|
|
90
|
+
*/
|
|
91
|
+
function virtualPath(p) {
|
|
92
|
+
if (p.charCodeAt(0) !== 1)
|
|
93
|
+
return p;
|
|
94
|
+
let out = '';
|
|
95
|
+
for (let i = 1; i < p.length; i++) {
|
|
96
|
+
const c = p.charCodeAt(i);
|
|
97
|
+
if (c === 1) {
|
|
98
|
+
// A drive letter, or @ for a UNC host. A U+0003 after it starts the path.
|
|
99
|
+
const v = p[i + 1] ?? '';
|
|
100
|
+
i++;
|
|
101
|
+
out += v === '@' ? '\\\\' : `${v}:`;
|
|
102
|
+
}
|
|
103
|
+
else if (c === 2 || c === 3)
|
|
104
|
+
out += '\\';
|
|
105
|
+
else if (c === 4)
|
|
106
|
+
out += '..\\';
|
|
107
|
+
// A long volume, such as a URL, after a character that counts it.
|
|
108
|
+
else if (c === 5)
|
|
109
|
+
i++;
|
|
110
|
+
else if (c >= 6 && c <= 8)
|
|
111
|
+
out += 'XLSTART\\';
|
|
112
|
+
else
|
|
113
|
+
out += p[i];
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
/** One sheet's substream, with any chart substreams inside it: Obj records that run macros, and hyperlinks. */
|
|
118
|
+
async function sheetRules(ctx, cfb, stream, s, decrypt, location) {
|
|
119
|
+
const records = new biff_1.RecordReader(cfb, stream, s.position, ctx.settings.deadline);
|
|
120
|
+
const bof = await records.next();
|
|
121
|
+
if (bof?.type !== biff_1.R.bof || bof.body.length < 4)
|
|
122
|
+
return;
|
|
123
|
+
const bofType = bof.body[2] | (bof.body[3] << 8);
|
|
124
|
+
// The sheet's own BOF says what it is, and so does its BoundSheet record. Either one makes a macro sheet.
|
|
125
|
+
if (s.type === 1 || bofType === 0x0040)
|
|
126
|
+
ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.XlmMacrosheet, location, { sheet: (0, context_1.clip)(s.name), state: STATES[s.state] });
|
|
127
|
+
let depth = 1;
|
|
128
|
+
for (let r = await records.next(); r && depth > 0; r = await records.next()) {
|
|
129
|
+
if (r.type === biff_1.R.bof)
|
|
130
|
+
depth++;
|
|
131
|
+
else if (r.type === biff_1.R.eof)
|
|
132
|
+
depth--;
|
|
133
|
+
else if (r.type === biff_1.R.obj) {
|
|
134
|
+
if (objMacro(decrypt(r)))
|
|
135
|
+
ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.Link, location, { sheet: (0, context_1.clip)(s.name) });
|
|
136
|
+
}
|
|
137
|
+
else if (r.type === biff_1.R.hlink) {
|
|
138
|
+
const target = hyperlink(decrypt(r));
|
|
139
|
+
if (target.trim() && !(0, links_1.isFragment)(target))
|
|
140
|
+
ctx.link(target, location, { sheet: (0, context_1.clip)(s.name), source: 'HLink record' });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/** Whether an Obj record (MS-XLS 2.4.181) holds an FtMacro subrecord, the macro a click runs. */
|
|
145
|
+
function objMacro(b) {
|
|
146
|
+
let at = 0;
|
|
147
|
+
while (at + 4 <= b.length) {
|
|
148
|
+
const ft = b[at] | (b[at + 1] << 8);
|
|
149
|
+
const cb = b[at + 2] | (b[at + 3] << 8);
|
|
150
|
+
// FtEnd ends the list, and FtLbsData's length does not count all its data, so the walk stops there.
|
|
151
|
+
if (ft === 0x00 || ft === 0x13)
|
|
152
|
+
return false;
|
|
153
|
+
if (ft === 0x04)
|
|
154
|
+
return cb > 0;
|
|
155
|
+
at += 4 + cb;
|
|
156
|
+
}
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
/** The target of an HLink record: its Hyperlink Object (MS-OSHARED 2.3.7.1) after the cell range and class id. */
|
|
160
|
+
function hyperlink(b) {
|
|
161
|
+
const r = new oleobj_1.ByteReader(b);
|
|
162
|
+
return (0, oleobj_1.bounded)(() => {
|
|
163
|
+
r.skip(8 + 16 + 4);
|
|
164
|
+
const flags = r.u32();
|
|
165
|
+
const string = () => (0, oleobj_1.utf16)(r.bytes(r.u32() * 2)).replace(/\0+$/, '');
|
|
166
|
+
if (flags & 0x10)
|
|
167
|
+
string();
|
|
168
|
+
if (flags & 0x80)
|
|
169
|
+
string();
|
|
170
|
+
if (!(flags & 0x01))
|
|
171
|
+
return '';
|
|
172
|
+
return flags & 0x100 ? string() : (0, oleobj_1.readMoniker)(r).target;
|
|
173
|
+
}, () => '');
|
|
174
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@patrickdk77/office-defuse",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.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",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"LICENSE"
|
|
23
23
|
],
|
|
24
24
|
"engines": {
|
|
25
|
-
"node": ">=
|
|
25
|
+
"node": ">=18.0.0"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true});require('fs').rmSync('build-test',{recursive:true,force:true})\"",
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"@biomejs/biome": "^2.5.15",
|
|
85
85
|
"@types/chai": "^5.2.3",
|
|
86
86
|
"@types/mocha": "^10.0.10",
|
|
87
|
-
"@types/node": "^
|
|
87
|
+
"@types/node": "^18.19.130",
|
|
88
88
|
"c8": "^12.0.0",
|
|
89
89
|
"chai": "^6.3.0",
|
|
90
90
|
"husky": "^9.1.7",
|