@patrickdk77/office-defuse 0.2.2 → 0.3.0
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 +89 -11
- package/dist/biff.d.ts +11 -0
- package/dist/biff.js +15 -0
- package/dist/contained.d.ts +15 -5
- package/dist/contained.js +11 -7
- package/dist/context.d.ts +20 -6
- package/dist/context.js +28 -3
- package/dist/engine.js +7 -6
- package/dist/fields.js +1 -1
- package/dist/findings.js +9 -3
- package/dist/flat.js +4 -7
- package/dist/forms.d.ts +51 -0
- package/dist/forms.js +316 -0
- package/dist/macros.d.ts +26 -0
- package/dist/macros.js +160 -0
- package/dist/msg.js +1 -1
- package/dist/objects.js +76 -17
- package/dist/odf.js +9 -10
- package/dist/ole.d.ts +3 -3
- package/dist/ole.js +55 -5
- package/dist/ooxml.d.ts +26 -0
- package/dist/ooxml.js +163 -15
- package/dist/ppt-writer.d.ts +27 -0
- package/dist/ppt-writer.js +105 -0
- package/dist/ppt.d.ts +5 -1
- package/dist/ppt.js +179 -46
- package/dist/rtf.js +4 -2
- package/dist/score.d.ts +2 -0
- package/dist/score.js +12 -0
- package/dist/types.d.ts +44 -0
- package/dist/types.js +3 -0
- package/dist/vba.d.ts +78 -0
- package/dist/vba.js +378 -0
- package/dist/xls-writer.d.ts +34 -0
- package/dist/xls-writer.js +250 -0
- package/dist/xls.d.ts +9 -1
- package/dist/xls.js +126 -43
- package/package.json +1 -1
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RewriteError = exports.hasEdits = exports.noEdits = void 0;
|
|
4
|
+
exports.rewriteWorkbook = rewriteWorkbook;
|
|
5
|
+
const biff_1 = require("./biff");
|
|
6
|
+
const noEdits = () => ({ emptySheets: new Set(), names: new Map(), objMacros: new Set(), selfBooks: new Set(), hyperlinks: new Set(), decrypt: false });
|
|
7
|
+
exports.noEdits = noEdits;
|
|
8
|
+
const hasEdits = (e) => e.decrypt || e.emptySheets.size > 0 || e.names.size > 0 || e.objMacros.size > 0 || e.selfBooks.size > 0 || e.hyperlinks.size > 0;
|
|
9
|
+
exports.hasEdits = hasEdits;
|
|
10
|
+
/** A rewrite that cannot keep the stream's offsets right, which leaves the stream as it was. */
|
|
11
|
+
class RewriteError extends Error {
|
|
12
|
+
}
|
|
13
|
+
exports.RewriteError = RewriteError;
|
|
14
|
+
const T = { index: 0x020b, dbcell: 0x00d7, extsst: 0x00ff, defColWidth: 0x0055, dimensions: 0x0200, window2: 0x023e, hlinkTooltip: 0x0800 };
|
|
15
|
+
/** Records that hold stream positions: BoundSheet8, Index, DBCell and ExtSST. */
|
|
16
|
+
const POINTERS = new Set([biff_1.R.boundSheet, T.index, T.dbcell, T.extsst]);
|
|
17
|
+
/** BIFF8's largest record body. */
|
|
18
|
+
const MAX_BODY = 8224;
|
|
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
|
+
const put32 = (b, at, v) => {
|
|
22
|
+
b[at] = v & 0xff;
|
|
23
|
+
b[at + 1] = (v >>> 8) & 0xff;
|
|
24
|
+
b[at + 2] = (v >>> 16) & 0xff;
|
|
25
|
+
b[at + 3] = (v >>> 24) & 0xff;
|
|
26
|
+
};
|
|
27
|
+
const record = (type, body) => ({ type, body, old: -1 });
|
|
28
|
+
/**
|
|
29
|
+
* Writes a workbook stream again into `sink`, record by record in the order the stream holds them, with the edits
|
|
30
|
+
* made. Records change size, so the stream positions other records hold are written anew: BoundSheet8's lbPlyPos,
|
|
31
|
+
* Index's ibXF and rgibRw, DBCell's dbRtrw and ExtSST's ib. A position that does not fall where a record of the stream
|
|
32
|
+
* starts, or inside one that keeps its bytes, throws RewriteError before anything is written. `decrypt` turns each
|
|
33
|
+
* record after FILEPASS into its plain bytes. Bytes after the last whole record are not written.
|
|
34
|
+
*
|
|
35
|
+
* The stream is read twice: once for the record headers and the few bodies the edits change, and once to copy the
|
|
36
|
+
* rest, so the rewrite holds the records' headers, not their bytes.
|
|
37
|
+
*/
|
|
38
|
+
async function rewriteWorkbook(cfb, stream, edits, biff8, decrypt, sink, deadline) {
|
|
39
|
+
const heads = [];
|
|
40
|
+
/** The plain bodies the first pass needs: each BOF, the records that hold stream positions, and those the edits change. */
|
|
41
|
+
const bodies = new Map();
|
|
42
|
+
let filePass = -1;
|
|
43
|
+
const plainOf = (r, i) => Uint8Array.from(filePass >= 0 && i > filePass && decrypt ? decrypt(r) : r.body);
|
|
44
|
+
const reader = new biff_1.RecordReader(cfb, stream, 0, deadline);
|
|
45
|
+
for (let r = await reader.next(); r; r = await reader.next()) {
|
|
46
|
+
const i = heads.length;
|
|
47
|
+
heads.push({ type: r.type, offset: r.offset, length: r.body.length });
|
|
48
|
+
// office-defuse does not encrypt, so an encrypted workbook is written without FILEPASS, every record decrypted.
|
|
49
|
+
if (r.type === biff_1.R.filePass && filePass < 0) {
|
|
50
|
+
if (!decrypt)
|
|
51
|
+
throw new RewriteError('an encrypted workbook without its key');
|
|
52
|
+
filePass = i;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (r.type === biff_1.R.bof || POINTERS.has(r.type) || edits.names.has(r.offset) || edits.objMacros.has(r.offset) || edits.selfBooks.has(r.offset))
|
|
56
|
+
bodies.set(r.offset, plainOf(r, i));
|
|
57
|
+
}
|
|
58
|
+
if (heads[0]?.type !== biff_1.R.bof)
|
|
59
|
+
throw new RewriteError('the stream does not start with a BOF record');
|
|
60
|
+
const indexOf = new Map(heads.map((h, i) => [h.offset, i]));
|
|
61
|
+
// Each substream, from a BOF at the top level to the EOF that closes it, by the offset of its BOF.
|
|
62
|
+
const ends = new Map();
|
|
63
|
+
let depth = 0;
|
|
64
|
+
let open = -1;
|
|
65
|
+
heads.forEach((h, i) => {
|
|
66
|
+
if (h.type === biff_1.R.bof) {
|
|
67
|
+
if (depth === 0)
|
|
68
|
+
open = i;
|
|
69
|
+
depth++;
|
|
70
|
+
}
|
|
71
|
+
else if (h.type === biff_1.R.eof && depth > 0 && --depth === 0)
|
|
72
|
+
ends.set(heads[open].offset, i);
|
|
73
|
+
});
|
|
74
|
+
const empty = new Map();
|
|
75
|
+
for (const at of edits.emptySheets) {
|
|
76
|
+
const body = bodies.get(at);
|
|
77
|
+
if (!body || heads[indexOf.get(at) ?? -1]?.type !== biff_1.R.boundSheet)
|
|
78
|
+
throw new RewriteError('an edit names no BoundSheet record');
|
|
79
|
+
const start = u32(body, 0);
|
|
80
|
+
const end = ends.get(start);
|
|
81
|
+
if (end === undefined)
|
|
82
|
+
throw new RewriteError('a sheet whose position is not where a substream starts');
|
|
83
|
+
empty.set(start, end);
|
|
84
|
+
}
|
|
85
|
+
const out = [];
|
|
86
|
+
for (let i = 0; i < heads.length; i++) {
|
|
87
|
+
const h = heads[i];
|
|
88
|
+
if (i === filePass)
|
|
89
|
+
continue;
|
|
90
|
+
if (edits.hyperlinks.has(h.offset)) {
|
|
91
|
+
if (heads[i + 1]?.type === T.hlinkTooltip)
|
|
92
|
+
i++;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const end = empty.get(h.offset);
|
|
96
|
+
if (end !== undefined) {
|
|
97
|
+
emptyWorksheet(bodies.get(h.offset) ?? new Uint8Array(0), biff8).forEach((x, k) => {
|
|
98
|
+
out.push({ type: x.type, source: -1, old: k === 0 ? h.offset : -1, length: x.body.length, body: x.body });
|
|
99
|
+
});
|
|
100
|
+
i = end;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
let body = bodies.get(h.offset);
|
|
104
|
+
let changed = false;
|
|
105
|
+
if (body && h.type === biff_1.R.boundSheet && edits.emptySheets.has(h.offset)) {
|
|
106
|
+
body = Uint8Array.from(body);
|
|
107
|
+
body[5] = 0;
|
|
108
|
+
changed = true;
|
|
109
|
+
}
|
|
110
|
+
const name = edits.names.get(h.offset);
|
|
111
|
+
if (body && name !== undefined)
|
|
112
|
+
[body, changed] = [renamed(body, name, biff8), true];
|
|
113
|
+
if (body && edits.objMacros.has(h.offset))
|
|
114
|
+
[body, changed] = [withoutMacro(body), true];
|
|
115
|
+
if (body && edits.selfBooks.has(h.offset))
|
|
116
|
+
[body, changed] = [selfBook(body), true];
|
|
117
|
+
const length = changed && body ? body.length : h.length;
|
|
118
|
+
if (length > MAX_BODY)
|
|
119
|
+
throw new RewriteError('a record past 8,224 bytes');
|
|
120
|
+
out.push({ type: h.type, source: i, old: h.offset, length, body: changed ? body : undefined });
|
|
121
|
+
}
|
|
122
|
+
// New offsets, and the old offset of each record kept as it was, so positions can be followed.
|
|
123
|
+
const moved = new Map();
|
|
124
|
+
const kept = [];
|
|
125
|
+
let pos = 0;
|
|
126
|
+
for (const x of out) {
|
|
127
|
+
if (x.old >= 0) {
|
|
128
|
+
moved.set(x.old, pos);
|
|
129
|
+
if (x.source >= 0 && !x.body)
|
|
130
|
+
kept.push({ old: x.old, now: pos, length: 4 + x.length });
|
|
131
|
+
}
|
|
132
|
+
pos += 4 + x.length;
|
|
133
|
+
}
|
|
134
|
+
const start = (old) => {
|
|
135
|
+
const now = moved.get(old);
|
|
136
|
+
if (now === undefined)
|
|
137
|
+
throw new RewriteError(`a stream position, ${old}, where no record starts`);
|
|
138
|
+
return now;
|
|
139
|
+
};
|
|
140
|
+
// Kept records are in stream order, so the one around a position is found by halving.
|
|
141
|
+
const inside = (old) => {
|
|
142
|
+
let lo = 0;
|
|
143
|
+
let hi = kept.length - 1;
|
|
144
|
+
while (lo < hi) {
|
|
145
|
+
const mid = (lo + hi + 1) >> 1;
|
|
146
|
+
if (kept[mid].old <= old)
|
|
147
|
+
lo = mid;
|
|
148
|
+
else
|
|
149
|
+
hi = mid - 1;
|
|
150
|
+
}
|
|
151
|
+
const k = kept[lo];
|
|
152
|
+
if (!k || old < k.old || old >= k.old + k.length)
|
|
153
|
+
throw new RewriteError(`a stream position, ${old}, inside no record the rewrite keeps as it was`);
|
|
154
|
+
return k.now + (old - k.old);
|
|
155
|
+
};
|
|
156
|
+
const patched = (x, b, at) => {
|
|
157
|
+
if (x.type === biff_1.R.boundSheet && b.length >= 4)
|
|
158
|
+
put32(b, 0, start(u32(b, 0)));
|
|
159
|
+
else if (x.type === T.index && b.length >= (biff8 ? 16 : 12)) {
|
|
160
|
+
// BIFF8 writes rwMic and rwMac in 4 bytes each, BIFF5 in 2.
|
|
161
|
+
const ib = biff8 ? 12 : 8;
|
|
162
|
+
if (u32(b, ib))
|
|
163
|
+
put32(b, ib, start(u32(b, ib)));
|
|
164
|
+
for (let k = ib + 4; k + 4 <= b.length; k += 4)
|
|
165
|
+
put32(b, k, start(u32(b, k)));
|
|
166
|
+
}
|
|
167
|
+
else if (x.type === T.dbcell && b.length >= 4 && x.old >= 0)
|
|
168
|
+
put32(b, 0, at - start(x.old - u32(b, 0)));
|
|
169
|
+
else if (x.type === T.extsst)
|
|
170
|
+
for (let k = 2; k + 8 <= b.length; k += 8)
|
|
171
|
+
put32(b, k, inside(u32(b, k)));
|
|
172
|
+
return b;
|
|
173
|
+
};
|
|
174
|
+
// The records that hold positions are written anew here, so a position that does not resolve fails before the
|
|
175
|
+
// second pass writes anything.
|
|
176
|
+
for (const x of out) {
|
|
177
|
+
if (x.source < 0 || !POINTERS.has(x.type))
|
|
178
|
+
continue;
|
|
179
|
+
const base = x.body ?? bodies.get(x.old);
|
|
180
|
+
if (!base)
|
|
181
|
+
throw new RewriteError('a record that holds stream positions was not read');
|
|
182
|
+
x.body = patched(x, Uint8Array.from(base), moved.get(x.old) ?? 0);
|
|
183
|
+
}
|
|
184
|
+
const again = new biff_1.RecordReader(cfb, stream, 0, deadline);
|
|
185
|
+
let index = -1;
|
|
186
|
+
let current;
|
|
187
|
+
for (const x of out) {
|
|
188
|
+
let b;
|
|
189
|
+
if (x.body)
|
|
190
|
+
b = Uint8Array.from(x.body);
|
|
191
|
+
else {
|
|
192
|
+
while (index < x.source) {
|
|
193
|
+
current = await again.next();
|
|
194
|
+
index++;
|
|
195
|
+
if (!current)
|
|
196
|
+
throw new RewriteError('the stream changed while it was read');
|
|
197
|
+
}
|
|
198
|
+
b = plainOf(current, index);
|
|
199
|
+
}
|
|
200
|
+
await sink.write(Uint8Array.of(x.type & 0xff, x.type >> 8, b.length & 0xff, b.length >> 8));
|
|
201
|
+
await sink.write(b);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* The substream of an empty worksheet, from a sheet's BOF: the BOF saying worksheet, a default column width, empty
|
|
206
|
+
* dimensions, a window and EOF.
|
|
207
|
+
*/
|
|
208
|
+
function emptyWorksheet(bof, biff8) {
|
|
209
|
+
const head = Uint8Array.from(bof);
|
|
210
|
+
if (head.length >= 4) {
|
|
211
|
+
head[2] = 0x10;
|
|
212
|
+
head[3] = 0x00;
|
|
213
|
+
}
|
|
214
|
+
// BIFF5's Dimensions and Window2 are shorter: 2-byte rows, and a window without its zoom fields.
|
|
215
|
+
const window = new Uint8Array(biff8 ? 18 : 10);
|
|
216
|
+
window[0] = 0xb6;
|
|
217
|
+
window[6] = 0x40;
|
|
218
|
+
return [record(biff_1.R.bof, head), record(T.defColWidth, Uint8Array.of(8, 0)), record(T.dimensions, new Uint8Array(biff8 ? 14 : 10)), record(T.window2, window), record(biff_1.R.eof, new Uint8Array(0))];
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* An Lbl record (MS-XLS 2.4.150) under another name, which is not a built-in one. BIFF8 writes the name after a flag
|
|
222
|
+
* byte, and BIFF5 writes its characters alone.
|
|
223
|
+
*/
|
|
224
|
+
function renamed(b, name, biff8) {
|
|
225
|
+
const cch = b[3];
|
|
226
|
+
const wide = biff8 && (b[14] & 1) === 1;
|
|
227
|
+
const after = 14 + (biff8 ? 1 : 0) + cch * (wide ? 2 : 1);
|
|
228
|
+
if (after > b.length)
|
|
229
|
+
throw new RewriteError('an Lbl record cut short');
|
|
230
|
+
const head = Uint8Array.from(b.subarray(0, 14));
|
|
231
|
+
head[0] &= ~0x20;
|
|
232
|
+
head[3] = name.length;
|
|
233
|
+
return Uint8Array.from([...head, ...(biff8 ? [0] : []), ...Buffer.from(name, 'latin1'), ...b.subarray(after)]);
|
|
234
|
+
}
|
|
235
|
+
/** An Obj record (MS-XLS 2.4.181) without its FtMacro subrecord. */
|
|
236
|
+
function withoutMacro(b) {
|
|
237
|
+
for (const { ft, at, data } of (0, biff_1.objSubrecords)(b))
|
|
238
|
+
if (ft === 0x04)
|
|
239
|
+
return Buffer.concat([b.subarray(0, at), b.subarray(at + 4 + data.length)]);
|
|
240
|
+
throw new RewriteError('an Obj record without the FtMacro to remove');
|
|
241
|
+
}
|
|
242
|
+
/** A SupBook record (MS-XLS 2.4.271) whose VirtualPath is U+0002, the workbook itself, with its sheet names kept. */
|
|
243
|
+
function selfBook(b) {
|
|
244
|
+
const cch = u16(b, 2);
|
|
245
|
+
const wide = (b[4] & 1) === 1;
|
|
246
|
+
const after = 4 + 1 + cch * (wide ? 2 : 1);
|
|
247
|
+
if (cch === 0 || cch >= 0x0401 || after > b.length)
|
|
248
|
+
throw new RewriteError('a SupBook record without a path');
|
|
249
|
+
return Uint8Array.from([b[0], b[1], 1, 0, 0, 2, ...b.subarray(after)]);
|
|
250
|
+
}
|
package/dist/xls.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type BiffRecord } from './biff';
|
|
2
2
|
import type { CfbEntry, CompoundFile } from './cfb';
|
|
3
3
|
import { type RunContext } from './context';
|
|
4
|
+
import type { CompoundPlan } from './plan';
|
|
4
5
|
/** Turns an encrypted record body into its plain bytes. Without encryption the body is as stored. */
|
|
5
6
|
export type Decrypt = (r: BiffRecord) => Uint8Array;
|
|
6
7
|
/**
|
|
@@ -23,7 +24,14 @@ export declare function workbookKind(cfb: CompoundFile, stream: CfbEntry, deadli
|
|
|
23
24
|
* names Excel runs as macros, and the DDE, OLE and workbook links. The sheets give shapes that run macros and
|
|
24
25
|
* hyperlinks. An encrypted workbook goes through `unlock`.
|
|
25
26
|
*/
|
|
26
|
-
|
|
27
|
+
/** An ActiveX control a sheet's Obj record keeps in the Ctls stream: its ProgID and where its data is. */
|
|
28
|
+
export interface CtlsControl {
|
|
29
|
+
progId: string;
|
|
30
|
+
position: number;
|
|
31
|
+
size: number;
|
|
32
|
+
sheet: string;
|
|
33
|
+
}
|
|
34
|
+
export declare function excelRules(ctx: RunContext, cfb: CompoundFile, stream: CfbEntry, unlock: Unlock, plan?: CompoundPlan, controls?: CtlsControl[]): Promise<void>;
|
|
27
35
|
/**
|
|
28
36
|
* A VirtualPath (MS-XLS 2.5.277) written out. One that starts with U+0001 is encoded: its control characters name a
|
|
29
37
|
* volume, the root, a folder, a step up, a URL's volume or Excel's startup folders.
|
package/dist/xls.js
CHANGED
|
@@ -8,9 +8,11 @@ const biff_1 = require("./biff");
|
|
|
8
8
|
const context_1 = require("./context");
|
|
9
9
|
const errors_1 = require("./errors");
|
|
10
10
|
const formulas_1 = require("./formulas");
|
|
11
|
+
const io_1 = require("./io");
|
|
11
12
|
const links_1 = require("./links");
|
|
12
13
|
const oleobj_1 = require("./oleobj");
|
|
13
14
|
const types_1 = require("./types");
|
|
15
|
+
const xls_writer_1 = require("./xls-writer");
|
|
14
16
|
const STATES = ['visible', 'hidden', 'very hidden', 'hidden'];
|
|
15
17
|
/** Record types of cell formulas (MS-XLS 2.4.127, 2.4.260 and 2.4.4): FORMULA, SHRFMLA and ARRAY. */
|
|
16
18
|
const FORMULA = 0x0006;
|
|
@@ -41,13 +43,7 @@ async function workbookKind(cfb, stream, deadline) {
|
|
|
41
43
|
}
|
|
42
44
|
return kind;
|
|
43
45
|
}
|
|
44
|
-
|
|
45
|
-
* Inspects an Excel 5.0 to 2003 workbook stream. Excel reads the globals substream from the start of the stream,
|
|
46
|
-
* then each sheet from where its BoundSheet record says the sheet starts. The globals give the macro sheets, the
|
|
47
|
-
* names Excel runs as macros, and the DDE, OLE and workbook links. The sheets give shapes that run macros and
|
|
48
|
-
* hyperlinks. An encrypted workbook goes through `unlock`.
|
|
49
|
-
*/
|
|
50
|
-
async function excelRules(ctx, cfb, stream, unlock) {
|
|
46
|
+
async function excelRules(ctx, cfb, stream, unlock, plan, controls = []) {
|
|
51
47
|
const location = stream.name;
|
|
52
48
|
const records = new biff_1.RecordReader(cfb, stream, 0, ctx.settings.deadline);
|
|
53
49
|
const first = await records.next();
|
|
@@ -60,24 +56,32 @@ async function excelRules(ctx, cfb, stream, unlock) {
|
|
|
60
56
|
const names = [];
|
|
61
57
|
/** The SupBook each XTI of the ExternSheet record names. */
|
|
62
58
|
const xti = [];
|
|
59
|
+
const edits = (0, xls_writer_1.noEdits)();
|
|
60
|
+
/** Names that run a macro and go, by their Lbl record, given new names once every name is known. */
|
|
61
|
+
const renames = [];
|
|
63
62
|
for (let r = await records.next(); r && r.type !== biff_1.R.eof; r = await records.next()) {
|
|
64
63
|
if (r.type === biff_1.R.filePass) {
|
|
64
|
+
const before = ctx.findings.length;
|
|
65
65
|
decrypt = await unlock(ctx, r, location);
|
|
66
|
+
// The rewrite writes the workbook decrypted, so one VelvetSweatshop opens is rewritten when that finding strips.
|
|
67
|
+
if (ctx.findings.slice(before).some(f => f.category === types_1.OfficeCategory.Encrypted && f.detail === types_1.OfficeDetail.DefaultPassword && f.action === 'strip'))
|
|
68
|
+
edits.decrypt = true;
|
|
66
69
|
continue;
|
|
67
70
|
}
|
|
68
71
|
const body = decrypt(r);
|
|
69
72
|
if (r.type === biff_1.R.boundSheet)
|
|
70
|
-
sheets.push((0, biff_1.parseBoundSheet)(body, biff8));
|
|
73
|
+
sheets.push({ ...(0, biff_1.parseBoundSheet)(body, biff8), record: r.offset });
|
|
71
74
|
else if (r.type === biff_1.R.lbl) {
|
|
72
75
|
const name = (0, biff_1.parseLbl)(body, biff8);
|
|
73
76
|
names.push(name.name);
|
|
74
77
|
const auto = (0, biff_1.autoName)(name.name);
|
|
75
|
-
|
|
76
|
-
|
|
78
|
+
// Removing the name would renumber the names after it, which formulas refer to by number, so it is renamed.
|
|
79
|
+
if (auto && 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) }).action === 'strip')
|
|
80
|
+
renames.push({ offset: r.offset, event: auto.event });
|
|
77
81
|
}
|
|
78
82
|
else if (r.type === biff_1.R.supBook && body.length >= 4) {
|
|
79
83
|
const cch = body[2] | (body[3] << 8);
|
|
80
|
-
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, names: [] });
|
|
84
|
+
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, names: [], offset: r.offset });
|
|
81
85
|
}
|
|
82
86
|
else if (r.type === biff_1.R.externSheet && biff8 && body.length >= 2) {
|
|
83
87
|
const n = body[0] | (body[1] << 8);
|
|
@@ -95,31 +99,64 @@ async function excelRules(ctx, cfb, stream, unlock) {
|
|
|
95
99
|
books[books.length - 1].dde = true;
|
|
96
100
|
}
|
|
97
101
|
}
|
|
102
|
+
// A link that goes keeps its SupBook, which formulas count to by index, with a path to the workbook itself.
|
|
98
103
|
for (const b of books)
|
|
99
|
-
linkRules(ctx, b, location)
|
|
104
|
+
if (linkRules(ctx, b, location)?.action === 'strip')
|
|
105
|
+
edits.selfBooks.add(b.offset);
|
|
100
106
|
const nameOf = (ref) => (ref.kind === 'name' ? names[ref.index - 1] : books[xti[ref.ixti] ?? -1]?.names[ref.index - 1]);
|
|
107
|
+
// A macro of the workbook's own: an external name of the SupBook that is the workbook itself, as Excel names a VBA
|
|
108
|
+
// macro a button runs. A defined name may stand for anything, so it is not one.
|
|
109
|
+
const ownName = (ref) => (ref.kind === 'namex' && books[xti[ref.ixti] ?? -1]?.cch === 0x401 ? nameOf(ref) : undefined);
|
|
101
110
|
for (const s of sheets)
|
|
102
|
-
await sheetRules(ctx, cfb, stream, s, decrypt, location, biff8 ? nameOf : undefined);
|
|
111
|
+
await sheetRules(ctx, cfb, stream, s, decrypt, location, edits, controls, biff8 ? nameOf : undefined, biff8 ? ownName : undefined);
|
|
112
|
+
const taken = new Set(names.map(n => n.toLowerCase()));
|
|
113
|
+
for (const { offset, event } of renames) {
|
|
114
|
+
let k = 1;
|
|
115
|
+
while (taken.has(`removed_${event}_${k}`.toLowerCase()))
|
|
116
|
+
k++;
|
|
117
|
+
const name = `Removed_${event}_${k}`;
|
|
118
|
+
taken.add(name.toLowerCase());
|
|
119
|
+
edits.names.set(offset, name);
|
|
120
|
+
}
|
|
121
|
+
// BIFF5 has no SupBook, HLink or Obj macro records in the shape BIFF8 has, so only its sheets and names change. What
|
|
122
|
+
// else strips stays, and the check of the output says so.
|
|
123
|
+
if (!biff8) {
|
|
124
|
+
edits.objMacros.clear();
|
|
125
|
+
edits.selfBooks.clear();
|
|
126
|
+
edits.hyperlinks.clear();
|
|
127
|
+
}
|
|
128
|
+
if (!plan || !(0, xls_writer_1.hasEdits)(edits))
|
|
129
|
+
return;
|
|
130
|
+
const sink = new io_1.SpillSink(ctx.run.temp, ctx.run.threshold);
|
|
131
|
+
ctx.run.held.push(sink);
|
|
132
|
+
try {
|
|
133
|
+
await (0, xls_writer_1.rewriteWorkbook)(cfb, stream, edits, biff8, decrypt, sink, ctx.settings.deadline);
|
|
134
|
+
await sink.close();
|
|
135
|
+
plan.replace.set(stream, sink);
|
|
136
|
+
}
|
|
137
|
+
catch (e) {
|
|
138
|
+
// Left as it is, the stream still holds what the findings name, and the check of the output says so.
|
|
139
|
+
if (!(e instanceof xls_writer_1.RewriteError))
|
|
140
|
+
throw e;
|
|
141
|
+
}
|
|
103
142
|
}
|
|
104
|
-
/**
|
|
143
|
+
/**
|
|
144
|
+
* A SupBook record, read as MS-XLS 2.4.271 and oletools tell its kinds apart. A path that starts with U+0002 or U+0003
|
|
145
|
+
* refers to the workbook itself, as LibreOffice reads it.
|
|
146
|
+
*/
|
|
105
147
|
function linkRules(ctx, b, location) {
|
|
106
|
-
if (b.cch === 0x401 || b.cch === 0x3a01 || b.path === ' ' || b.path === '\0' || !b.path)
|
|
107
|
-
return;
|
|
148
|
+
if (b.cch === 0x401 || b.cch === 0x3a01 || b.path === ' ' || b.path === '\0' || !b.path || b.path.charCodeAt(0) === 2 || b.path.charCodeAt(0) === 3)
|
|
149
|
+
return undefined;
|
|
108
150
|
if (b.ctab === 0) {
|
|
109
151
|
// A DDE or OLE data source: the application or class, U+0003, and the topic or file.
|
|
110
152
|
const [application, topic = ''] = b.path.split('\x03');
|
|
111
153
|
if (b.ole && !b.dde) {
|
|
112
154
|
const data = { source: 'SupBook', className: (0, context_1.clip)(application) };
|
|
113
|
-
|
|
114
|
-
ctx.external(types_1.OfficeDetail.Object, topic, location, data, 'linked object');
|
|
115
|
-
else
|
|
116
|
-
ctx.report(types_1.OfficeCategory.External, types_1.OfficeDetail.Object, location, data);
|
|
155
|
+
return topic ? ctx.external(types_1.OfficeDetail.Object, topic, location, data, 'linked object') : ctx.report(types_1.OfficeCategory.External, types_1.OfficeDetail.Object, location, data);
|
|
117
156
|
}
|
|
118
|
-
|
|
119
|
-
ctx.report(types_1.OfficeCategory.Dde, types_1.OfficeDetail.Link, location, { application: (0, context_1.clip)(application), topic: (0, context_1.clip)(topic) });
|
|
120
|
-
return;
|
|
157
|
+
return ctx.report(types_1.OfficeCategory.Dde, types_1.OfficeDetail.Link, location, { application: (0, context_1.clip)(application), topic: (0, context_1.clip)(topic) });
|
|
121
158
|
}
|
|
122
|
-
ctx.external(types_1.OfficeDetail.Workbook, virtualPath(b.path), location, { source: 'SupBook' }, 'external workbook');
|
|
159
|
+
return ctx.external(types_1.OfficeDetail.Workbook, virtualPath(b.path), location, { source: 'SupBook' }, 'external workbook');
|
|
123
160
|
}
|
|
124
161
|
/**
|
|
125
162
|
* A VirtualPath (MS-XLS 2.5.277) written out. One that starts with U+0001 is encoded: its control characters name a
|
|
@@ -152,7 +189,7 @@ function virtualPath(p) {
|
|
|
152
189
|
return out;
|
|
153
190
|
}
|
|
154
191
|
/** One sheet's substream, with any chart substreams inside it: Obj records that run macros, and hyperlinks. */
|
|
155
|
-
async function sheetRules(ctx, cfb, stream, s, decrypt, location, nameOf) {
|
|
192
|
+
async function sheetRules(ctx, cfb, stream, s, decrypt, location, edits, controls, nameOf, ownName) {
|
|
156
193
|
const records = new biff_1.RecordReader(cfb, stream, s.position, ctx.settings.deadline);
|
|
157
194
|
const bof = await records.next();
|
|
158
195
|
if (bof?.type !== biff_1.R.bof || bof.body.length < 4)
|
|
@@ -160,8 +197,9 @@ async function sheetRules(ctx, cfb, stream, s, decrypt, location, nameOf) {
|
|
|
160
197
|
const bofType = bof.body[2] | (bof.body[3] << 8);
|
|
161
198
|
// The sheet's own BOF says what it is, and so does its BoundSheet record. Either one makes a macro sheet.
|
|
162
199
|
const macroSheet = s.type === 1 || bofType === 0x0040;
|
|
163
|
-
|
|
164
|
-
|
|
200
|
+
// A macro sheet becomes an empty worksheet of the same name, so the sheets after it keep their indexes.
|
|
201
|
+
if (macroSheet && ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.XlmMacrosheet, location, { sheet: (0, context_1.clip)(s.name), state: STATES[s.state] }).action === 'strip' && s.record !== undefined)
|
|
202
|
+
edits.emptySheets.add(s.record);
|
|
165
203
|
let depth = 1;
|
|
166
204
|
for (let r = await records.next(); r && depth > 0; r = await records.next()) {
|
|
167
205
|
if (r.type === biff_1.R.bof)
|
|
@@ -169,13 +207,23 @@ async function sheetRules(ctx, cfb, stream, s, decrypt, location, nameOf) {
|
|
|
169
207
|
else if (r.type === biff_1.R.eof)
|
|
170
208
|
depth--;
|
|
171
209
|
else if (r.type === biff_1.R.obj) {
|
|
172
|
-
|
|
173
|
-
|
|
210
|
+
const body = decrypt(r);
|
|
211
|
+
const control = ctlsControl(body);
|
|
212
|
+
if (control)
|
|
213
|
+
controls.push({ ...control, sheet: s.name });
|
|
214
|
+
const macro = objMacro(body);
|
|
215
|
+
if (!macro)
|
|
216
|
+
continue;
|
|
217
|
+
// A shape that runs one of the workbook's own macros stays when its VBA project does.
|
|
218
|
+
const name = macro.ref && ownName ? ownName(macro.ref) : undefined;
|
|
219
|
+
const data = { sheet: (0, context_1.clip)(s.name), ...(macro.ref && nameOf ? { macro: (0, context_1.clip)(nameOf(macro.ref) ?? '') } : {}) };
|
|
220
|
+
if (ctx.macroTrigger(types_1.OfficeCategory.Macro, types_1.OfficeDetail.Link, location, data, name).action === 'strip')
|
|
221
|
+
edits.objMacros.add(r.offset);
|
|
174
222
|
}
|
|
175
223
|
else if (r.type === biff_1.R.hlink) {
|
|
176
224
|
const target = hyperlink(decrypt(r));
|
|
177
|
-
if (target.trim() && !(0, links_1.isFragment)(target))
|
|
178
|
-
|
|
225
|
+
if (target.trim() && !(0, links_1.isFragment)(target) && ctx.link(target, location, { sheet: (0, context_1.clip)(s.name), source: 'HLink record' }).action === 'strip')
|
|
226
|
+
edits.hyperlinks.add(r.offset);
|
|
179
227
|
}
|
|
180
228
|
else if (nameOf && !macroSheet && FORMULA_AT.has(r.type)) {
|
|
181
229
|
// A macro sheet's formulas are its macros, which its own finding covers.
|
|
@@ -189,20 +237,55 @@ async function sheetRules(ctx, cfb, stream, s, decrypt, location, nameOf) {
|
|
|
189
237
|
}
|
|
190
238
|
}
|
|
191
239
|
}
|
|
192
|
-
/**
|
|
240
|
+
/**
|
|
241
|
+
* The FtMacro subrecord of an Obj record (MS-XLS 2.4.181), the macro a click runs, with the name its formula refers
|
|
242
|
+
* to when the formula is one PtgName or PtgNameX and nothing else. Undefined when the record has none.
|
|
243
|
+
*/
|
|
193
244
|
function objMacro(b) {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
245
|
+
for (const { ft, data: f } of (0, biff_1.objSubrecords)(b)) {
|
|
246
|
+
if (ft !== 0x04)
|
|
247
|
+
continue;
|
|
248
|
+
if (f.length === 0)
|
|
249
|
+
return undefined;
|
|
250
|
+
// As Excel writes it: cce in 15 bits, 4 unused bytes, and the tokens. PtgName and PtgNameX are classed tokens,
|
|
251
|
+
// 0x23, 0x43 or 0x63 and 0x39, 0x59 or 0x79; a base token below 0x20 shares their low bits and is neither.
|
|
252
|
+
const cce = f.length >= 2 ? (f[0] | (f[1] << 8)) & 0x7fff : 0;
|
|
253
|
+
const rgce = f.subarray(6, 6 + cce);
|
|
254
|
+
const ptg = rgce[0] ?? 0;
|
|
255
|
+
const u32 = (k) => (rgce[k] | (rgce[k + 1] << 8) | (rgce[k + 2] << 16) | (rgce[k + 3] << 24)) >>> 0;
|
|
256
|
+
if (ptg >= 0x20 && (ptg & 0x1f) === 0x03 && cce === 5 && rgce.length === 5)
|
|
257
|
+
return { ref: { kind: 'name', index: u32(1) } };
|
|
258
|
+
if (ptg >= 0x20 && (ptg & 0x1f) === 0x19 && cce === 7 && rgce.length === 7)
|
|
259
|
+
return { ref: { kind: 'namex', ixti: rgce[1] | (rgce[2] << 8), index: u32(3) } };
|
|
260
|
+
return {};
|
|
261
|
+
}
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* The control an Obj record keeps in the Ctls stream: FtPioGrbit says an ActiveX control (fCtl) whose data is in Ctls
|
|
266
|
+
* (fPrstm), and FtPictFmla gives its ProgID, in the formula's embedInfo, and its place in Ctls after the formula.
|
|
267
|
+
*/
|
|
268
|
+
function ctlsControl(b) {
|
|
269
|
+
let flags = 0;
|
|
270
|
+
for (const { ft, data: d } of (0, biff_1.objSubrecords)(b)) {
|
|
271
|
+
if (ft === 0x08 && d.length >= 2)
|
|
272
|
+
flags = d[0] | (d[1] << 8);
|
|
273
|
+
if (ft !== 0x09 || (flags & 0x30) !== 0x30 || d.length < 2)
|
|
274
|
+
continue;
|
|
275
|
+
// ObjFmla: cbFmla, then cce, 4 unused bytes, the tokens, and PictFmlaEmbedInfo: 0x03, cbClass, a reserved byte
|
|
276
|
+
// and the class name. lPosInCtlStm and cbBufInCtlStm follow.
|
|
277
|
+
const cbFmla = d[0] | (d[1] << 8);
|
|
278
|
+
const f = d.subarray(2, 2 + cbFmla);
|
|
279
|
+
const cce = f.length >= 2 ? (f[0] | (f[1] << 8)) & 0x7fff : 0;
|
|
280
|
+
const info = f.subarray(6 + cce);
|
|
281
|
+
const progId = info[0] === 0x03 && info.length >= 4 ? (0, biff_1.xlString)(info, 3, info[1]).text : '';
|
|
282
|
+
const after = 2 + cbFmla;
|
|
283
|
+
if (after + 8 > d.length)
|
|
284
|
+
return undefined;
|
|
285
|
+
const u32 = (k) => (d[k] | (d[k + 1] << 8) | (d[k + 2] << 16) | (d[k + 3] << 24)) >>> 0;
|
|
286
|
+
return { progId, position: u32(after), size: u32(after + 4) };
|
|
204
287
|
}
|
|
205
|
-
return
|
|
288
|
+
return undefined;
|
|
206
289
|
}
|
|
207
290
|
/** The target of an HLink record: its Hyperlink Object (MS-OSHARED 2.3.7.1) after the cell range and class id. */
|
|
208
291
|
function hyperlink(b) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@patrickdk77/office-defuse",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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",
|