@patrickdk77/office-defuse 0.2.1 → 0.2.4
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 +82 -10
- package/dist/biff.d.ts +11 -0
- package/dist/biff.js +15 -0
- package/dist/contained.d.ts +5 -0
- package/dist/contained.js +1 -0
- package/dist/context.d.ts +10 -0
- package/dist/context.js +10 -0
- package/dist/engine.js +2 -0
- package/dist/fields.js +1 -1
- package/dist/findings.js +7 -1
- package/dist/forms.d.ts +51 -0
- package/dist/forms.js +316 -0
- package/dist/macros.d.ts +21 -0
- package/dist/macros.js +149 -0
- package/dist/objects.js +50 -12
- package/dist/ole.d.ts +3 -3
- package/dist/ole.js +55 -5
- package/dist/ooxml.d.ts +26 -0
- package/dist/ooxml.js +164 -10
- 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 +177 -46
- package/dist/rewrite-zip.js +2 -1
- 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/dist/xml.d.ts +3 -0
- package/dist/xml.js +19 -1
- package/package.json +1 -1
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.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.Link, location, data, name === undefined ? undefined : (0, context_1.ownMacro)(ctx.run, 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/dist/xml.d.ts
CHANGED
|
@@ -159,6 +159,9 @@ export declare class XmlTokenizer {
|
|
|
159
159
|
/** The XML declaration says standalone="yes". */
|
|
160
160
|
private standalone;
|
|
161
161
|
private encoding;
|
|
162
|
+
/** Set by a declaration of US-ASCII, which agrees with UTF-8 only while every byte is ASCII. */
|
|
163
|
+
private asciiDeclared;
|
|
164
|
+
private nonAscii;
|
|
162
165
|
/** Start tags read so far, which numbers the elements. */
|
|
163
166
|
private elements;
|
|
164
167
|
/** An attribute value of the tag being read referred to an entity of the internal subset. */
|
package/dist/xml.js
CHANGED
|
@@ -190,6 +190,9 @@ class XmlTokenizer {
|
|
|
190
190
|
/** The XML declaration says standalone="yes". */
|
|
191
191
|
standalone = false;
|
|
192
192
|
encoding = 'utf-8';
|
|
193
|
+
/** Set by a declaration of US-ASCII, which agrees with UTF-8 only while every byte is ASCII. */
|
|
194
|
+
asciiDeclared = false;
|
|
195
|
+
nonAscii = false;
|
|
193
196
|
/** Start tags read so far, which numbers the elements. */
|
|
194
197
|
elements = 0;
|
|
195
198
|
/** An attribute value of the tag being read referred to an entity of the internal subset. */
|
|
@@ -263,6 +266,14 @@ class XmlTokenizer {
|
|
|
263
266
|
return decoder;
|
|
264
267
|
}
|
|
265
268
|
decode(decoder, bytes, more) {
|
|
269
|
+
if (!this.nonAscii)
|
|
270
|
+
for (const c of bytes)
|
|
271
|
+
if (c >= 0x80) {
|
|
272
|
+
this.nonAscii = true;
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
if (this.asciiDeclared && this.nonAscii)
|
|
276
|
+
this.stop(types_1.OfficeDetail.Encoding, 'The declaration says us-ascii, the bytes are not all ASCII');
|
|
266
277
|
try {
|
|
267
278
|
return decoder.decode(bytes, { stream: more });
|
|
268
279
|
}
|
|
@@ -279,9 +290,16 @@ class XmlTokenizer {
|
|
|
279
290
|
const declared = /\bencoding\s*=\s*(["'])([^"']*)\1/.exec(body)?.[2]?.toLowerCase();
|
|
280
291
|
if (declared === undefined)
|
|
281
292
|
return;
|
|
282
|
-
|
|
293
|
+
// SharePoint declares us-ascii on the custom XML it writes. ASCII bytes read the same in UTF-8.
|
|
294
|
+
const ascii = this.encoding === 'utf-8' && (declared === 'us-ascii' || declared === 'ascii');
|
|
295
|
+
const ok = this.encoding === 'utf-8' ? declared === 'utf-8' || declared === 'utf8' || ascii : declared === 'utf-16' || declared === this.encoding;
|
|
283
296
|
if (!ok)
|
|
284
297
|
this.stop(types_1.OfficeDetail.Encoding, `The declaration says ${declared}, the bytes are ${this.encoding.toUpperCase()}`);
|
|
298
|
+
if (ascii) {
|
|
299
|
+
this.asciiDeclared = true;
|
|
300
|
+
if (this.nonAscii)
|
|
301
|
+
this.stop(types_1.OfficeDetail.Encoding, 'The declaration says us-ascii, the bytes are not all ASCII');
|
|
302
|
+
}
|
|
285
303
|
}
|
|
286
304
|
feed(text, final) {
|
|
287
305
|
this.buf = this.pos < this.buf.length ? this.buf.slice(this.pos) + text : text;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@patrickdk77/office-defuse",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
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",
|