@patrickdk77/office-defuse 0.0.0-stage → 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.
Files changed (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +317 -2
  3. package/dist/biff.d.ts +62 -0
  4. package/dist/biff.js +95 -0
  5. package/dist/cfb.d.ts +58 -0
  6. package/dist/cfb.js +456 -0
  7. package/dist/context.d.ts +66 -0
  8. package/dist/context.js +121 -0
  9. package/dist/crc32.d.ts +8 -0
  10. package/dist/crc32.js +58 -0
  11. package/dist/engine.d.ts +5 -0
  12. package/dist/engine.js +200 -0
  13. package/dist/errors.d.ts +19 -0
  14. package/dist/errors.js +33 -0
  15. package/dist/fields.d.ts +29 -0
  16. package/dist/fields.js +97 -0
  17. package/dist/findings.d.ts +25 -0
  18. package/dist/findings.js +188 -0
  19. package/dist/formats.d.ts +10 -0
  20. package/dist/formats.js +46 -0
  21. package/dist/formulas.d.ts +22 -0
  22. package/dist/formulas.js +77 -0
  23. package/dist/index.d.ts +5 -0
  24. package/dist/index.js +30 -0
  25. package/dist/index.mjs +3 -0
  26. package/dist/io.d.ts +23 -0
  27. package/dist/io.js +139 -0
  28. package/dist/links.d.ts +26 -0
  29. package/dist/links.js +145 -0
  30. package/dist/msg.d.ts +7 -0
  31. package/dist/msg.js +97 -0
  32. package/dist/objects.d.ts +42 -0
  33. package/dist/objects.js +134 -0
  34. package/dist/odf.d.ts +3 -0
  35. package/dist/odf.js +297 -0
  36. package/dist/offcrypto.d.ts +31 -0
  37. package/dist/offcrypto.js +408 -0
  38. package/dist/ole.d.ts +12 -0
  39. package/dist/ole.js +127 -0
  40. package/dist/oleobj.d.ts +74 -0
  41. package/dist/oleobj.js +198 -0
  42. package/dist/ooxml.d.ts +8 -0
  43. package/dist/ooxml.js +468 -0
  44. package/dist/package.d.ts +3 -0
  45. package/dist/package.js +21 -0
  46. package/dist/ppt.d.ts +8 -0
  47. package/dist/ppt.js +241 -0
  48. package/dist/rtf.d.ts +8 -0
  49. package/dist/rtf.js +539 -0
  50. package/dist/score.d.ts +17 -0
  51. package/dist/score.js +63 -0
  52. package/dist/types.d.ts +274 -0
  53. package/dist/types.js +152 -0
  54. package/dist/word.d.ts +9 -0
  55. package/dist/word.js +160 -0
  56. package/dist/xls.d.ts +27 -0
  57. package/dist/xls.js +174 -0
  58. package/dist/xlsb.d.ts +19 -0
  59. package/dist/xlsb.js +101 -0
  60. package/dist/xml.d.ts +256 -0
  61. package/dist/xml.js +1105 -0
  62. package/dist/zip-writer.d.ts +38 -0
  63. package/dist/zip-writer.js +182 -0
  64. package/dist/zip.d.ts +55 -0
  65. package/dist/zip.js +535 -0
  66. package/package.json +96 -4
package/dist/links.js ADDED
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isAbsoluteTarget = exports.isInsidePackage = exports.isLocalTarget = exports.isFragment = void 0;
4
+ exports.checkLink = checkLink;
5
+ const types_1 = require("./types");
6
+ const xml_1 = require("./xml");
7
+ const IPV4 = /^\d{1,3}(\.\d{1,3}){3}$/;
8
+ const SCHEME = /^([a-zA-Z][a-zA-Z0-9+.-]*):/;
9
+ /** A browser drops control characters from a URL, and so does this before reading one. */
10
+ const clean = (s) => s.replace(/\p{Cc}/gu, '').trim();
11
+ const networkPrefix = (s) => s.startsWith('\\\\') || s.startsWith('//') || s.startsWith('\\/') || s.startsWith('/\\');
12
+ /** A target that names the same document or a place in it. RFC 3986 reads an empty reference as the document itself. */
13
+ const isFragment = (target) => {
14
+ const t = clean(target);
15
+ return t === '' || t.startsWith('#');
16
+ };
17
+ exports.isFragment = isFragment;
18
+ /**
19
+ * Classifies a hyperlink target with pdf-defuse's checks, without the comparison of the target with the link's text.
20
+ * Call it only for targets that are more than a fragment. A target that holds the content of an external entity
21
+ * cannot be read, and reads as the empty string: a relative link, which is what pdf-defuse makes of a URI it cannot
22
+ * resolve.
23
+ */
24
+ function checkLink(rawTarget) {
25
+ const uri = rawTarget.includes(xml_1.UNKNOWN) ? '' : clean(rawTarget);
26
+ const link = (detail, site) => ({ category: types_1.OfficeCategory.Link, detail, host: site });
27
+ if (networkPrefix(uri))
28
+ return link(types_1.OfficeDetail.NetworkPath);
29
+ const m = SCHEME.exec(uri);
30
+ if (!m)
31
+ return link(types_1.OfficeDetail.Relative);
32
+ const scheme = m[1].toLowerCase();
33
+ // C:\path is a drive, not a scheme.
34
+ if (scheme.length === 1)
35
+ return link(types_1.OfficeDetail.FileUrl);
36
+ if (scheme === 'javascript' || scheme === 'vbscript')
37
+ return link(types_1.OfficeDetail.ScriptUrl);
38
+ if (scheme === 'vnd.sun.star.script' || scheme === 'macro')
39
+ return { category: types_1.OfficeCategory.Macro, detail: types_1.OfficeDetail.Link };
40
+ if (scheme === 'data')
41
+ return link(types_1.OfficeDetail.DataUrl);
42
+ if (scheme === 'smb' || scheme === 'cifs' || scheme === 'afp' || scheme === 'nfs')
43
+ return link(types_1.OfficeDetail.NetworkPath);
44
+ if (scheme === 'file') {
45
+ let host = '';
46
+ try {
47
+ host = new URL(uri).hostname;
48
+ }
49
+ catch {
50
+ // A file: URL the parser refuses is read as a local path.
51
+ }
52
+ return link(host && host.toLowerCase() !== 'localhost' ? types_1.OfficeDetail.NetworkPath : types_1.OfficeDetail.FileUrl);
53
+ }
54
+ if (scheme === 'mailto') {
55
+ let addr = uri.slice(7).split('?')[0];
56
+ try {
57
+ addr = decodeURIComponent(addr);
58
+ }
59
+ catch {
60
+ // The address stays as written.
61
+ }
62
+ const domain = addr.includes('@') ? addr.slice(addr.lastIndexOf('@') + 1).toLowerCase() : '';
63
+ return link(hostVerdict(domain, domain) ?? types_1.OfficeDetail.Safe, domain);
64
+ }
65
+ if (scheme !== 'http' && scheme !== 'https')
66
+ return link(types_1.OfficeDetail.OtherScheme);
67
+ // The authority as written, before the URL parser normalizes it.
68
+ const rawAuth = uri
69
+ .slice(m[0].length)
70
+ .replace(/^[\\/]*/, '')
71
+ .split(/[/\\?#]/, 1)[0];
72
+ let parsed;
73
+ try {
74
+ parsed = new URL(uri);
75
+ }
76
+ catch {
77
+ return link(types_1.OfficeDetail.OtherScheme);
78
+ }
79
+ if (parsed.username || parsed.password || rawAuth.includes('@'))
80
+ return link(types_1.OfficeDetail.Credentials);
81
+ // The parser lowercases, punycodes and turns every IPv4 shorthand into dotted form, as browsers do.
82
+ const host = parsed.hostname.replace(/\.$/, '');
83
+ return link(hostVerdict(host, rawAuth) ?? types_1.OfficeDetail.Safe, host);
84
+ }
85
+ function hostVerdict(host, raw) {
86
+ if (!host)
87
+ return types_1.OfficeDetail.OtherScheme;
88
+ if (raw.includes('%'))
89
+ return types_1.OfficeDetail.EncodedHost;
90
+ if (host.startsWith('[') || IPV4.test(host))
91
+ return types_1.OfficeDetail.IpHost;
92
+ if (/^\d+$/.test(host) || /^0x[0-9a-f]+$/i.test(host))
93
+ return types_1.OfficeDetail.IpHost;
94
+ if (/\P{ASCII}/u.test(host) || host.split('.').some(label => label.startsWith('xn--')))
95
+ return types_1.OfficeDetail.LookalikeHost;
96
+ return undefined;
97
+ }
98
+ /**
99
+ * The target with every %XX turned into its character. A target can percent-encode its scheme, as `%73cript:` does in
100
+ * a CVE-2017-0199 sample, so the checks below hold a target local or inside the package only when both forms are.
101
+ */
102
+ const decoded = (s) => s.replace(/%([0-9a-fA-F]{2})/g, (_, hex) => String.fromCharCode(Number.parseInt(hex, 16)));
103
+ function local(t) {
104
+ if (networkPrefix(t))
105
+ return false;
106
+ const m = SCHEME.exec(t);
107
+ if (!m || m[1].length === 1)
108
+ return true;
109
+ if (m[1].toLowerCase() !== 'file')
110
+ return false;
111
+ try {
112
+ const host = new URL(t).hostname;
113
+ return !host || host.toLowerCase() === 'localhost';
114
+ }
115
+ catch {
116
+ return true;
117
+ }
118
+ }
119
+ /**
120
+ * Whether a target outside the file is on this machine: a drive path, a file: URL without a host, an absolute path or
121
+ * a relative one. A UNC path, a file: URL with a host and every other scheme reach elsewhere, and so can a target that
122
+ * holds the content of an external entity.
123
+ */
124
+ const isLocalTarget = (rawTarget) => !rawTarget.includes(xml_1.UNKNOWN) && local(clean(rawTarget)) && local(clean(decoded(rawTarget)));
125
+ exports.isLocalTarget = isLocalTarget;
126
+ function inside(t) {
127
+ if (t === '' || networkPrefix(t) || SCHEME.test(t) || t.startsWith('/') || t.startsWith('\\'))
128
+ return false;
129
+ let depth = 0;
130
+ for (const seg of t.split(/[/\\]/)) {
131
+ if (seg === '..')
132
+ depth--;
133
+ else if (seg !== '.' && seg !== '')
134
+ depth++;
135
+ if (depth < 0)
136
+ return false;
137
+ }
138
+ return true;
139
+ }
140
+ /** Whether an ODF reference names an entry inside the package: a relative path that does not climb out of it. */
141
+ const isInsidePackage = (rawHref) => inside(clean(rawHref)) && inside(clean(decoded(rawHref)));
142
+ exports.isInsidePackage = isInsidePackage;
143
+ /** Whether an OOXML relationship target is not a part name, whatever TargetMode says: a scheme, a drive or a network path. */
144
+ const isAbsoluteTarget = (rawTarget) => [rawTarget, decoded(rawTarget)].some(t => SCHEME.test(clean(t)) || t.startsWith('\\') || t.startsWith('//'));
145
+ exports.isAbsoluteTarget = isAbsoluteTarget;
package/dist/msg.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ import { type CfbEntry, type CompoundFile } from './cfb';
2
+ import { type RunContext } from './context';
3
+ /**
4
+ * Inspects an Outlook message (MS-OXMSG): each attachment is reported with its name and type, and an attachment by
5
+ * reference with what it points to. An embedded message's own attachments are read as well.
6
+ */
7
+ export declare function messageRules(ctx: RunContext, cfb: CompoundFile, message: CfbEntry, depth?: number): Promise<void>;
package/dist/msg.js ADDED
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.messageRules = messageRules;
4
+ const cfb_1 = require("./cfb");
5
+ const context_1 = require("./context");
6
+ const oleobj_1 = require("./oleobj");
7
+ const types_1 = require("./types");
8
+ /** Property ids of an attachment (MS-OXPROPS). */
9
+ const P = { method: 0x3705, longName: 0x3707, name: 0x3704, display: 0x3001, mime: 0x370e, data: 0x3701, longPath: 0x370d, path: 0x3708 };
10
+ /** How an attachment holds its file (MS-OXCMSG 2.2.2.9): by value, by reference, embedded message, OLE object, web reference. */
11
+ const METHODS = { 1: 'by value', 2: 'by reference', 3: 'by reference, resolved', 4: 'by reference only', 5: 'embedded message', 6: 'OLE object', 7: 'by web reference' };
12
+ /** Embedded messages can hold messages in turn. Past this depth they are still reported, and not read further. */
13
+ const MAX_DEPTH = 16;
14
+ /**
15
+ * Inspects an Outlook message (MS-OXMSG): each attachment is reported with its name and type, and an attachment by
16
+ * reference with what it points to. An embedded message's own attachments are read as well.
17
+ */
18
+ async function messageRules(ctx, cfb, message, depth = 0) {
19
+ for (const a of message.children) {
20
+ if (a.kind !== 'storage' || !/^__attach_version1\.0_#[0-9A-F]{8}$/i.test(a.name))
21
+ continue;
22
+ await attachmentRules(ctx, cfb, a, depth);
23
+ }
24
+ }
25
+ async function attachmentRules(ctx, cfb, a, depth) {
26
+ const location = (0, cfb_1.entryPath)(a);
27
+ const props = await properties(cfb, a);
28
+ const text = async (id) => {
29
+ for (const [type, decode] of [
30
+ ['001F', oleobj_1.utf16],
31
+ ['001E', oleobj_1.latin1],
32
+ ]) {
33
+ const s = a.children.find(c => c.kind === 'stream' && c.name.toUpperCase() === `__SUBSTG1.0_${id.toString(16).toUpperCase().padStart(4, '0')}${type}`);
34
+ if (s && s.size <= 65536)
35
+ return decode(await cfb.bytes(s)).replace(/\0+$/, '');
36
+ }
37
+ return '';
38
+ };
39
+ const method = props.get(P.method) ?? 0;
40
+ const name = (await text(P.longName)) || (await text(P.name)) || (await text(P.display));
41
+ const declaredType = await text(P.mime);
42
+ const dataId = `__SUBSTG1.0_${P.data.toString(16).toUpperCase()}`;
43
+ const data = a.children.find(c => c.name.toUpperCase().startsWith(dataId));
44
+ const info = { attachment: location, method: METHODS[method] ?? String(method) };
45
+ if (name)
46
+ info.name = (0, context_1.clip)(name);
47
+ if (declaredType)
48
+ info.type = (0, context_1.clip)(declaredType);
49
+ if (method >= 2 && method <= 4) {
50
+ const path = (await text(P.longPath)) || (await text(P.path));
51
+ if (path)
52
+ ctx.external(types_1.OfficeDetail.Other, path, location, { source: 'attachment by reference', ...(name ? { name: (0, context_1.clip)(name) } : {}) }, 'attachment');
53
+ return;
54
+ }
55
+ if (method === 7) {
56
+ const path = (await text(P.longPath)) || (await text(P.path));
57
+ if (path)
58
+ ctx.external(types_1.OfficeDetail.Other, path, location, { source: 'attachment by web reference', ...(name ? { name: (0, context_1.clip)(name) } : {}) }, 'attachment');
59
+ return;
60
+ }
61
+ if (!data)
62
+ return;
63
+ if (data.kind === 'stream') {
64
+ attachment(ctx, { name: name || undefined, declaredType: declaredType || undefined, size: data.size, location, depth: 1, source: cfb.source(data) }, info);
65
+ return;
66
+ }
67
+ // An embedded message or an OLE object is a storage of this file. The object rules read an object's storage, and
68
+ // an embedded message's attachments are read here.
69
+ info.size = 0;
70
+ if (method === 5)
71
+ info.type ??= 'application/vnd.ms-outlook';
72
+ ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Attachment, location, info);
73
+ if (method === 5 && depth < MAX_DEPTH)
74
+ await messageRules(ctx, cfb, data, depth + 1);
75
+ }
76
+ /**
77
+ * The place every attached file passes, with what a contained file plugin needs to read it. office-defuse reports it;
78
+ * the family's router, once office-defuse has a plugin for it, is where it would be handed on.
79
+ */
80
+ function attachment(ctx, file, info) {
81
+ ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Attachment, file.location, { ...info, size: file.size });
82
+ }
83
+ /** The fixed-size values of a property stream (MS-OXMSG 2.4.2), by property id. An attachment's has an 8-byte header. */
84
+ async function properties(cfb, storage) {
85
+ const out = new Map();
86
+ const s = storage.children.find(c => c.kind === 'stream' && c.name.toUpperCase() === '__PROPERTIES_VERSION1.0');
87
+ if (!s || s.size > 65536)
88
+ return out;
89
+ const b = await cfb.bytes(s);
90
+ for (let at = 8; at + 16 <= b.length; at += 16) {
91
+ const tag = (b[at] | (b[at + 1] << 8) | (b[at + 2] << 16) | (b[at + 3] << 24)) >>> 0;
92
+ // PT_LONG values sit in the first four bytes of the value.
93
+ if ((tag & 0xffff) === 0x0003)
94
+ out.set(tag >>> 16, (b[at + 8] | (b[at + 9] << 8) | (b[at + 10] << 16) | (b[at + 11] << 24)) >>> 0);
95
+ }
96
+ return out;
97
+ }
@@ -0,0 +1,42 @@
1
+ import { type CfbEntry, type CompoundFile } from './cfb';
2
+ import { type RunContext } from './context';
3
+ import { type PackagedFile } from './oleobj';
4
+ /** Equation Editor 3.0's ProgID, which rtfobj matches as a prefix. */
5
+ export declare const isEquationClass: (name: string) => boolean;
6
+ /** Reports what the reader of a compound file inside this one noted, at the place it sits. */
7
+ export declare function nestedNotes(ctx: RunContext, cfb: CompoundFile, location: string): void;
8
+ /**
9
+ * The rules every compound file gets: VBA projects, embedded objects and controls, wherever they sit in the tree.
10
+ * `prefix` names where the file sits when it is inside another, and `rootIsObject` says that its root is itself an
11
+ * OLE object's storage, as an object in a presentation or an RTF file is.
12
+ */
13
+ export declare function compoundRules(ctx: RunContext, cfb: CompoundFile, prefix: string, rootIsObject: boolean): Promise<void>;
14
+ /** What an OLE object's storage says about the object. */
15
+ export interface ObjectInfo {
16
+ clsid: string;
17
+ progId?: string;
18
+ userType?: string;
19
+ /** An ActiveX control's storage, with the control's name. */
20
+ control: boolean;
21
+ controlName?: string;
22
+ linked: boolean;
23
+ /** What a linked object loads, or ''. */
24
+ target: string;
25
+ /** The target is a URL moniker's, which loads from outside however it is spelled. */
26
+ url: boolean;
27
+ /** A Packager object's file, held or linked to. */
28
+ pkg?: PackagedFile;
29
+ equation: boolean;
30
+ }
31
+ /** Reads an OLE object's storage: its class, its CompObj and OLE streams, and Packager's native data. */
32
+ export declare function describeObject(cfb: CompoundFile, e: CfbEntry, _location: string): Promise<ObjectInfo>;
33
+ /**
34
+ * Reports a linked object. A URL moniker's target loads from outside the file whatever its text looks like: a
35
+ * CVE-2017-0199 sample puts a character with a stray high byte where the colon of https: goes.
36
+ */
37
+ export declare function linkedObject(ctx: RunContext, d: {
38
+ target: string;
39
+ url: boolean;
40
+ }, location: string, data: Record<string, string>): void;
41
+ /** What a finding shows of a Packager object's file: its name and paths, and its size when the object holds it. */
42
+ export declare function packageData(p: PackagedFile): Record<string, string | number>;
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isEquationClass = void 0;
4
+ exports.nestedNotes = nestedNotes;
5
+ exports.compoundRules = compoundRules;
6
+ exports.describeObject = describeObject;
7
+ exports.linkedObject = linkedObject;
8
+ exports.packageData = packageData;
9
+ const cfb_1 = require("./cfb");
10
+ const context_1 = require("./context");
11
+ const oleobj_1 = require("./oleobj");
12
+ const types_1 = require("./types");
13
+ /** The class ids of Equation Editor 3.0 and its kin, which CVE-2017-11882 and CVE-2018-0802 exploit, as oletools lists them. */
14
+ const EQUATION_CLSIDS = new Set(['0002CE02-0000-0000-C000-000000000046', '00021700-0000-0000-C000-000000000046', '0003000B-0000-0000-C000-000000000046', '0004A6B0-0000-0000-C000-000000000046']);
15
+ /** Equation Editor 3.0's ProgID, which rtfobj matches as a prefix. */
16
+ const isEquationClass = (name) => name.trim().toLowerCase().startsWith('equation.3');
17
+ exports.isEquationClass = isEquationClass;
18
+ /** Streams that only an OLE object's storage holds. */
19
+ const OBJECT_STREAMS = new Set(['\x01OLE', '\x01COMPOBJ', '\x01OLE10NATIVE', '\x03OBJINFO']);
20
+ /** Streams that only an ActiveX control's storage holds. */
21
+ const CONTROL_STREAMS = new Set(['\x03OCXNAME', '\x03OCXDATA']);
22
+ /** A CompObj, OLE or ObjInfo stream is small. More is not read. */
23
+ const SMALL = 65536;
24
+ /** Packager's header is short, and its names are at most 1,024 bytes each. */
25
+ const PACKAGE_HEAD = 4096;
26
+ const upper = (e) => e.name.toUpperCase();
27
+ /** Reports what the reader of a compound file inside this one noted, at the place it sits. */
28
+ function nestedNotes(ctx, cfb, location) {
29
+ for (const n of cfb.notes)
30
+ ctx.report(types_1.OfficeCategory.Container, n.detail, location, { bytes: n.bytes });
31
+ }
32
+ /** Where a finding about an entry points: `prefix`, then the entry's path. */
33
+ const at = (prefix, e) => [prefix, (0, cfb_1.entryPath)(e)].filter(Boolean).join(' > ');
34
+ /**
35
+ * The rules every compound file gets: VBA projects, embedded objects and controls, wherever they sit in the tree.
36
+ * `prefix` names where the file sits when it is inside another, and `rootIsObject` says that its root is itself an
37
+ * OLE object's storage, as an object in a presentation or an RTF file is.
38
+ */
39
+ async function compoundRules(ctx, cfb, prefix, rootIsObject) {
40
+ const projects = new Set();
41
+ for (const e of cfb.entries) {
42
+ if (e.kind === 'stream')
43
+ continue;
44
+ const child = (name) => e.children.find(c => upper(c) === name);
45
+ // A VBA project: a VBA storage with a dir stream, which names the modules (MS-OVBA 2.2).
46
+ const vba = child('VBA');
47
+ if (vba?.kind === 'storage' && vba.children.some(c => c.kind === 'stream' && upper(c) === 'DIR')) {
48
+ projects.add(e);
49
+ ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.VbaProject, at(prefix, vba), { storage: at(prefix, vba) });
50
+ }
51
+ if (e.kind === 'storage' && (upper(e) === '_VBA_PROJECT_CUR' || upper(e) === 'MACROS') && e.children.length && !projects.has(e)) {
52
+ // Excel's and Word's project storages, without the dir stream a whole project has.
53
+ ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.VbaProject, at(prefix, e), { storage: at(prefix, e) });
54
+ }
55
+ const names = new Set(e.children.map(upper));
56
+ const parent = e.parent ? upper(e.parent) : '';
57
+ const isObject = e.kind === 'root'
58
+ ? rootIsObject
59
+ : [...names].some(n => OBJECT_STREAMS.has(n) || CONTROL_STREAMS.has(n)) ||
60
+ (parent === 'OBJECTPOOL' && e.parent?.parent?.kind === 'root') ||
61
+ (e.parent?.kind === 'root' && /^MBD[0-9A-F]{8}$/i.test(e.name));
62
+ if (isObject)
63
+ await objectRules(ctx, cfb, e, at(prefix, e));
64
+ }
65
+ }
66
+ /** Reads an OLE object's storage: its class, its CompObj and OLE streams, and Packager's native data. */
67
+ async function describeObject(cfb, e, _location) {
68
+ const child = (name) => e.children.find(c => c.kind === 'stream' && upper(c) === name);
69
+ const small = async (name) => {
70
+ const s = child(name);
71
+ return s && s.size <= SMALL ? cfb.bytes(s) : undefined;
72
+ };
73
+ const clsid = e.clsid === '00000000-0000-0000-0000-000000000000' ? '' : e.clsid;
74
+ const compObjBytes = await small('\x01COMPOBJ');
75
+ const comp = compObjBytes ? (0, oleobj_1.parseCompObj)(compObjBytes) : {};
76
+ const info = { clsid, progId: comp.progId, userType: comp.userType, control: false, linked: false, target: '', url: false, equation: false };
77
+ info.equation = EQUATION_CLSIDS.has(clsid) || (0, exports.isEquationClass)(comp.progId ?? '');
78
+ if ([...CONTROL_STREAMS].some(n => child(n))) {
79
+ const name = await small('\x03OCXNAME');
80
+ info.control = true;
81
+ if (name)
82
+ info.controlName = Buffer.from(name).toString('utf16le').replace(/\0+$/, '');
83
+ return info;
84
+ }
85
+ const oleBytes = await small('\x01OLE');
86
+ const ole = oleBytes ? (0, oleobj_1.parseOleStream)(oleBytes) : { linked: false, target: '', url: false };
87
+ Object.assign(info, ole);
88
+ const native = child('\x01OLE10NATIVE');
89
+ if (native) {
90
+ // The stream starts with the native data's size, then Packager's data.
91
+ info.pkg = (0, oleobj_1.parsePackage)(await cfb.source(native).read(4, PACKAGE_HEAD));
92
+ }
93
+ return info;
94
+ }
95
+ /** One OLE object's storage: a control, a linked object, or an embedded one, with what it holds. */
96
+ async function objectRules(ctx, cfb, e, location) {
97
+ const d = await describeObject(cfb, e, location);
98
+ const data = { storage: location };
99
+ if (d.clsid)
100
+ data.clsid = d.clsid;
101
+ if (d.progId)
102
+ data.progId = (0, context_1.clip)(d.progId);
103
+ if (d.userType)
104
+ data.className = (0, context_1.clip)(d.userType);
105
+ if (d.control) {
106
+ ctx.report(types_1.OfficeCategory.Control, types_1.OfficeDetail.ActiveX, location, d.controlName ? { ...data, name: (0, context_1.clip)(d.controlName) } : data);
107
+ return;
108
+ }
109
+ if (d.pkg) {
110
+ Object.assign(data, packageData(d.pkg));
111
+ if (!d.pkg.embedded && d.pkg.sourcePath)
112
+ ctx.external(types_1.OfficeDetail.Object, d.pkg.sourcePath, location, { storage: location }, 'package link');
113
+ }
114
+ if (d.linked)
115
+ linkedObject(ctx, d, location, { storage: location, ...(d.clsid ? { clsid: d.clsid } : {}) });
116
+ else
117
+ ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.OleObject, location, data);
118
+ if (d.equation)
119
+ ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Equation, location, data);
120
+ }
121
+ /**
122
+ * Reports a linked object. A URL moniker's target loads from outside the file whatever its text looks like: a
123
+ * CVE-2017-0199 sample puts a character with a stray high byte where the colon of https: goes.
124
+ */
125
+ function linkedObject(ctx, d, location, data) {
126
+ if (d.target && !d.url)
127
+ ctx.external(types_1.OfficeDetail.Object, d.target, location, data, 'linked object');
128
+ else
129
+ ctx.report(types_1.OfficeCategory.External, types_1.OfficeDetail.Object, location, d.target ? { ...data, target: (0, context_1.clip)(d.target, 2000) } : data);
130
+ }
131
+ /** What a finding shows of a Packager object's file: its name and paths, and its size when the object holds it. */
132
+ function packageData(p) {
133
+ return { fileName: (0, context_1.clip)(p.fileName), sourcePath: (0, context_1.clip)(p.sourcePath, 2000), tempPath: (0, context_1.clip)(p.tempPath, 2000), ...(p.embedded ? { size: p.size } : {}) };
134
+ }
package/dist/odf.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { type ZipContext } from './context';
2
+ /** Inspects an OpenDocument package. */
3
+ export declare function inspectOdf(ctx: ZipContext): Promise<void>;