@patrickdk77/office-defuse 0.1.0 → 0.2.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 +278 -36
- package/dist/biff.d.ts +62 -0
- package/dist/biff.js +95 -0
- package/dist/cfb-writer.d.ts +25 -0
- package/dist/cfb-writer.js +314 -0
- package/dist/cfb.d.ts +58 -0
- package/dist/cfb.js +456 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +357 -0
- package/dist/contained.d.ts +46 -0
- package/dist/contained.js +152 -0
- package/dist/context.d.ts +140 -17
- package/dist/context.js +135 -37
- package/dist/crc32.d.ts +8 -2
- package/dist/crc32.js +44 -4
- package/dist/engine.d.ts +12 -1
- package/dist/engine.js +336 -42
- package/dist/errors.d.ts +17 -0
- package/dist/errors.js +27 -1
- package/dist/fields.d.ts +36 -0
- package/dist/fields.js +139 -0
- package/dist/findings.d.ts +2 -1
- package/dist/findings.js +44 -9
- package/dist/flat.d.ts +33 -0
- package/dist/flat.js +486 -0
- package/dist/formats.d.ts +7 -0
- package/dist/formats.js +5 -1
- package/dist/formulas.d.ts +27 -0
- package/dist/formulas.js +157 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +8 -1
- package/dist/index.mjs +1 -1
- package/dist/io.d.ts +62 -1
- package/dist/io.js +245 -1
- package/dist/links.d.ts +27 -0
- package/dist/links.js +106 -1
- package/dist/msg.d.ts +8 -0
- package/dist/msg.js +251 -0
- package/dist/objects.d.ts +45 -0
- package/dist/objects.js +204 -0
- package/dist/odf.d.ts +66 -2
- package/dist/odf.js +181 -64
- package/dist/offcrypto.d.ts +54 -0
- package/dist/offcrypto.js +647 -0
- package/dist/ole.d.ts +12 -0
- package/dist/ole.js +139 -0
- package/dist/oleobj.d.ts +85 -0
- package/dist/oleobj.js +215 -0
- package/dist/ooxml.d.ts +83 -2
- package/dist/ooxml.js +358 -145
- package/dist/package.d.ts +3 -0
- package/dist/package.js +21 -0
- package/dist/plan.d.ts +104 -0
- package/dist/plan.js +73 -0
- package/dist/plugins.d.ts +8 -0
- package/dist/plugins.js +60 -0
- package/dist/ppt.d.ts +8 -0
- package/dist/ppt.js +281 -0
- package/dist/rewrite-cfb.d.ts +14 -0
- package/dist/rewrite-cfb.js +52 -0
- package/dist/rewrite-rtf.d.ts +9 -0
- package/dist/rewrite-rtf.js +45 -0
- package/dist/rewrite-xml.d.ts +36 -0
- package/dist/rewrite-xml.js +341 -0
- package/dist/rewrite-zip.d.ts +8 -0
- package/dist/rewrite-zip.js +498 -0
- package/dist/rtf.d.ts +8 -0
- package/dist/rtf.js +712 -0
- package/dist/rtfcomp.d.ts +7 -0
- package/dist/rtfcomp.js +80 -0
- package/dist/score.js +1 -1
- package/dist/sniff.d.ts +5 -0
- package/dist/sniff.js +38 -0
- package/dist/types.d.ts +66 -2
- package/dist/types.js +33 -0
- package/dist/vsd.d.ts +13 -0
- package/dist/vsd.js +689 -0
- package/dist/word.d.ts +9 -0
- package/dist/word.js +194 -0
- package/dist/xls.d.ts +27 -0
- package/dist/xls.js +208 -0
- package/dist/xml.d.ts +30 -0
- package/dist/xml.js +45 -23
- package/dist/zip-writer.d.ts +4 -0
- package/dist/zip-writer.js +20 -7
- package/dist/zip.d.ts +9 -2
- package/dist/zip.js +61 -4
- package/package.json +6 -3
package/dist/package.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.inspectPackage = inspectPackage;
|
|
4
|
+
const errors_1 = require("./errors");
|
|
5
|
+
const odf_1 = require("./odf");
|
|
6
|
+
const ooxml_1 = require("./ooxml");
|
|
7
|
+
const types_1 = require("./types");
|
|
8
|
+
/** Inspects an OOXML or OpenDocument package, which its markers name. */
|
|
9
|
+
async function inspectPackage(ctx) {
|
|
10
|
+
const zip = ctx.zip;
|
|
11
|
+
const contentTypes = zip.get('[Content_Types].xml');
|
|
12
|
+
const odf = zip.entries.some(e => e.name === 'mimetype') || zip.get('META-INF/manifest.xml') !== undefined;
|
|
13
|
+
if (contentTypes && odf)
|
|
14
|
+
throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.AmbiguousFormat, 'The file has both [Content_Types].xml and an OpenDocument mimetype or manifest', { reason: 'OOXML and OpenDocument markers' });
|
|
15
|
+
if (contentTypes)
|
|
16
|
+
await (0, ooxml_1.inspectOoxml)(ctx, contentTypes);
|
|
17
|
+
else if (odf)
|
|
18
|
+
await (0, odf_1.inspectOdf)(ctx);
|
|
19
|
+
else
|
|
20
|
+
throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.Unrecognized, 'The archive has neither [Content_Types].xml nor an OpenDocument mimetype or manifest', { reason: 'a plain ZIP archive' });
|
|
21
|
+
}
|
package/dist/plan.d.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { CfbEntry, CompoundFile } from './cfb';
|
|
2
|
+
import type { SpillSink } from './io';
|
|
3
|
+
import type { OoxmlGraph } from './ooxml';
|
|
4
|
+
import type { OfficeFinding } from './types';
|
|
5
|
+
import type { ZipEntry } from './zip';
|
|
6
|
+
/** What the rewrite does to one element of an XML part. */
|
|
7
|
+
export interface ElementEdit {
|
|
8
|
+
/** The element goes, with everything inside it. */
|
|
9
|
+
remove?: boolean;
|
|
10
|
+
/** Its tags go and its content stays, as with a hyperlink around text. */
|
|
11
|
+
unwrap?: boolean;
|
|
12
|
+
/** Attributes left out, each as its namespace URI, a space and its local name. */
|
|
13
|
+
strip?: Set<string>;
|
|
14
|
+
/** Attribute values replaced, by qualified name. */
|
|
15
|
+
set?: Map<string, string>;
|
|
16
|
+
/** The element's content replaced with this text, as with base64 data a plugin rewrote. */
|
|
17
|
+
content?: string;
|
|
18
|
+
}
|
|
19
|
+
/** The key `ElementEdit.strip` takes for an attribute. */
|
|
20
|
+
export declare const attributeKey: (ns: string, local: string) => string;
|
|
21
|
+
/** A Word field the rewrite removes, by the element numbers of its marks. Its result stays. */
|
|
22
|
+
export interface FieldMarks {
|
|
23
|
+
begin: number;
|
|
24
|
+
separate?: number;
|
|
25
|
+
end?: number;
|
|
26
|
+
/** A w:fldSimple, whose tags go and whose result runs stay. */
|
|
27
|
+
simple: boolean;
|
|
28
|
+
}
|
|
29
|
+
/** What the rewrite does inside one XML part. */
|
|
30
|
+
export interface PartPlan {
|
|
31
|
+
elements: Map<number, ElementEdit>;
|
|
32
|
+
fields: FieldMarks[];
|
|
33
|
+
/** The part has an internal subset that goes. Its entity references are expanded and its defaults written out. */
|
|
34
|
+
subset: boolean;
|
|
35
|
+
/** References to external entities in the part go. */
|
|
36
|
+
external: boolean;
|
|
37
|
+
/** A document type declaration without an internal subset goes. */
|
|
38
|
+
doctype: boolean;
|
|
39
|
+
/** The part refers to an external entity. */
|
|
40
|
+
hasExternal: boolean;
|
|
41
|
+
}
|
|
42
|
+
/** An OpenDocument object element whose fate waits for the plugins' verdict on the entry it names. */
|
|
43
|
+
export interface DeferredObject {
|
|
44
|
+
part: string;
|
|
45
|
+
index: number;
|
|
46
|
+
/** The key of the entry it refers to. */
|
|
47
|
+
target: string;
|
|
48
|
+
finding?: OfficeFinding;
|
|
49
|
+
strips: boolean;
|
|
50
|
+
}
|
|
51
|
+
/** Everything a strip finding names in a ZIP package, for the rewrite. */
|
|
52
|
+
export declare class PackagePlan {
|
|
53
|
+
readonly parts: Map<string, PartPlan>;
|
|
54
|
+
/** Entries that go with every reference to them, by nameKey. */
|
|
55
|
+
readonly drop: Set<string>;
|
|
56
|
+
/** OpenDocument folders that go whole, as nameKey prefixes ending in "/". */
|
|
57
|
+
readonly folders: Set<string>;
|
|
58
|
+
/** Relationship Ids that go, by the name of their relationships part. */
|
|
59
|
+
readonly rels: Map<string, Set<string>>;
|
|
60
|
+
/** Excel 4.0 macro sheets that become empty worksheets, by nameKey. */
|
|
61
|
+
readonly sheets: Set<string>;
|
|
62
|
+
/** Entries whose bytes a plugin replaced, by nameKey. */
|
|
63
|
+
readonly replace: Map<string, SpillSink>;
|
|
64
|
+
/** OpenDocument object references, decided once the plugins have seen the entries they name. */
|
|
65
|
+
readonly objects: DeferredObject[];
|
|
66
|
+
/** What the OOXML rules read of the package's parts and relationships. */
|
|
67
|
+
graph?: OoxmlGraph;
|
|
68
|
+
/** What the OpenDocument rules read of the package's layout. */
|
|
69
|
+
odf?: OdfLayout;
|
|
70
|
+
part(name: string): PartPlan;
|
|
71
|
+
/** Adds an edit to an element. Removing it covers every other edit. */
|
|
72
|
+
element(part: string, index: number, edit: ElementEdit): void;
|
|
73
|
+
dropRelationship(relsPart: string, id: string): void;
|
|
74
|
+
}
|
|
75
|
+
/** The entries of a ZIP package the rewrite needs besides the plan: what the format rules found. */
|
|
76
|
+
export interface OdfLayout {
|
|
77
|
+
mimetype?: ZipEntry;
|
|
78
|
+
manifest?: ZipEntry;
|
|
79
|
+
/** The media type the package declares, which the output's mimetype entry holds. */
|
|
80
|
+
mediaType: string;
|
|
81
|
+
}
|
|
82
|
+
/** Everything a strip finding names in a compound file, for the rewrite. */
|
|
83
|
+
export declare class CompoundPlan {
|
|
84
|
+
/** Streams and storages that go, each with everything under it. */
|
|
85
|
+
readonly drop: Set<CfbEntry>;
|
|
86
|
+
/** Streams whose content a plugin replaced. */
|
|
87
|
+
readonly replace: Map<CfbEntry, SpillSink>;
|
|
88
|
+
/** Storages whose content a plugin replaced with a compound file of its own, read from what it wrote. */
|
|
89
|
+
readonly grafts: Map<CfbEntry, CompoundFile>;
|
|
90
|
+
}
|
|
91
|
+
/** A range of an RTF file the rewrite leaves out, or replaces with `text` and then the bytes of the file at `copy`. */
|
|
92
|
+
export interface RtfCut {
|
|
93
|
+
start: number;
|
|
94
|
+
end: number;
|
|
95
|
+
text?: Uint8Array;
|
|
96
|
+
copy?: [number, number];
|
|
97
|
+
}
|
|
98
|
+
/** What the output of a Flat OPC package keeps of the file besides its parts. */
|
|
99
|
+
export interface FlatOpcLayout {
|
|
100
|
+
/** The prolog's mso-application processing instructions, which name the application that opens the file. */
|
|
101
|
+
instructions: string[];
|
|
102
|
+
/** Each part's attributes besides its name and content type, such as pkg:compression, by key. */
|
|
103
|
+
extra: Map<string, Array<[string, string]>>;
|
|
104
|
+
}
|
package/dist/plan.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CompoundPlan = exports.PackagePlan = exports.attributeKey = void 0;
|
|
4
|
+
/** The key `ElementEdit.strip` takes for an attribute. */
|
|
5
|
+
const attributeKey = (ns, local) => `${ns} ${local}`;
|
|
6
|
+
exports.attributeKey = attributeKey;
|
|
7
|
+
/** Everything a strip finding names in a ZIP package, for the rewrite. */
|
|
8
|
+
class PackagePlan {
|
|
9
|
+
parts = new Map();
|
|
10
|
+
/** Entries that go with every reference to them, by nameKey. */
|
|
11
|
+
drop = new Set();
|
|
12
|
+
/** OpenDocument folders that go whole, as nameKey prefixes ending in "/". */
|
|
13
|
+
folders = new Set();
|
|
14
|
+
/** Relationship Ids that go, by the name of their relationships part. */
|
|
15
|
+
rels = new Map();
|
|
16
|
+
/** Excel 4.0 macro sheets that become empty worksheets, by nameKey. */
|
|
17
|
+
sheets = new Set();
|
|
18
|
+
/** Entries whose bytes a plugin replaced, by nameKey. */
|
|
19
|
+
replace = new Map();
|
|
20
|
+
/** OpenDocument object references, decided once the plugins have seen the entries they name. */
|
|
21
|
+
objects = [];
|
|
22
|
+
/** What the OOXML rules read of the package's parts and relationships. */
|
|
23
|
+
graph;
|
|
24
|
+
/** What the OpenDocument rules read of the package's layout. */
|
|
25
|
+
odf;
|
|
26
|
+
part(name) {
|
|
27
|
+
let p = this.parts.get(name);
|
|
28
|
+
if (!p) {
|
|
29
|
+
p = { elements: new Map(), fields: [], subset: false, external: false, doctype: false, hasExternal: false };
|
|
30
|
+
this.parts.set(name, p);
|
|
31
|
+
}
|
|
32
|
+
return p;
|
|
33
|
+
}
|
|
34
|
+
/** Adds an edit to an element. Removing it covers every other edit. */
|
|
35
|
+
element(part, index, edit) {
|
|
36
|
+
const elements = this.part(part).elements;
|
|
37
|
+
const had = elements.get(index);
|
|
38
|
+
if (!had) {
|
|
39
|
+
elements.set(index, edit);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (had.remove || edit.remove) {
|
|
43
|
+
elements.set(index, { remove: true });
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
had.unwrap ||= edit.unwrap;
|
|
47
|
+
if (edit.strip)
|
|
48
|
+
had.strip = new Set([...(had.strip ?? []), ...edit.strip]);
|
|
49
|
+
if (edit.set)
|
|
50
|
+
had.set = new Map([...(had.set ?? []), ...edit.set]);
|
|
51
|
+
if (edit.content !== undefined)
|
|
52
|
+
had.content = edit.content;
|
|
53
|
+
}
|
|
54
|
+
dropRelationship(relsPart, id) {
|
|
55
|
+
let ids = this.rels.get(relsPart);
|
|
56
|
+
if (!ids) {
|
|
57
|
+
ids = new Set();
|
|
58
|
+
this.rels.set(relsPart, ids);
|
|
59
|
+
}
|
|
60
|
+
ids.add(id);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
exports.PackagePlan = PackagePlan;
|
|
64
|
+
/** Everything a strip finding names in a compound file, for the rewrite. */
|
|
65
|
+
class CompoundPlan {
|
|
66
|
+
/** Streams and storages that go, each with everything under it. */
|
|
67
|
+
drop = new Set();
|
|
68
|
+
/** Streams whose content a plugin replaced. */
|
|
69
|
+
replace = new Map();
|
|
70
|
+
/** Storages whose content a plugin replaced with a compound file of its own, read from what it wrote. */
|
|
71
|
+
grafts = new Map();
|
|
72
|
+
}
|
|
73
|
+
exports.CompoundPlan = CompoundPlan;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ContainedFilePlugin, OfficeOptions } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Defuses Office and OpenDocument files inside other files, with `options` when given. Without them, it takes the
|
|
4
|
+
* options of the office-defuse run that found the file, or none when another package found it. `limits.nestingDepth`
|
|
5
|
+
* applies to the depth in the context. The file's findings go into the containing file's report, and its output's
|
|
6
|
+
* findings into the output's report, each with its `weight`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function officePlugin(options?: OfficeOptions): ContainedFilePlugin;
|
package/dist/plugins.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.officePlugin = officePlugin;
|
|
4
|
+
const contained_1 = require("./contained");
|
|
5
|
+
const engine_1 = require("./engine");
|
|
6
|
+
const flat_1 = require("./flat");
|
|
7
|
+
const score_1 = require("./score");
|
|
8
|
+
const zip_1 = require("./zip");
|
|
9
|
+
/** What the ZIP reader needs to look for the markers of an Office package, with the default limits. */
|
|
10
|
+
const PROBE = { entries: 10_000, decompressedBytes: 512 * 1024 * 1024, totalDecompressedBytes: 2 * 1024 * 1024 * 1024 };
|
|
11
|
+
/**
|
|
12
|
+
* Whether the content is a file office-defuse reads: a compound file, RTF, a ZIP archive with the marker of an Office
|
|
13
|
+
* Open XML or OpenDocument package, or XML whose root is a flat OpenDocument document or a Flat OPC package. A ZIP
|
|
14
|
+
* archive without a marker, or one the ZIP reader refuses, is not.
|
|
15
|
+
*/
|
|
16
|
+
async function isOffice(source, sniffed) {
|
|
17
|
+
if (sniffed === 'application/x-ole-storage' || sniffed === 'application/rtf')
|
|
18
|
+
return true;
|
|
19
|
+
if (sniffed === 'application/xml') {
|
|
20
|
+
const head = await source.read(0, Math.min(16384, await source.size()));
|
|
21
|
+
return (0, flat_1.isFlatHead)(Buffer.from(head).toString('utf8'));
|
|
22
|
+
}
|
|
23
|
+
if (sniffed !== 'application/zip')
|
|
24
|
+
return false;
|
|
25
|
+
try {
|
|
26
|
+
const zip = await zip_1.ZipArchive.open(source, PROBE);
|
|
27
|
+
return zip.get('[Content_Types].xml') !== undefined || zip.entries.some(e => e.name === 'mimetype') || zip.get('META-INF/manifest.xml') !== undefined;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Defuses Office and OpenDocument files inside other files, with `options` when given. Without them, it takes the
|
|
35
|
+
* options of the office-defuse run that found the file, or none when another package found it. `limits.nestingDepth`
|
|
36
|
+
* applies to the depth in the context. The file's findings go into the containing file's report, and its output's
|
|
37
|
+
* findings into the output's report, each with its `weight`.
|
|
38
|
+
*/
|
|
39
|
+
function officePlugin(options) {
|
|
40
|
+
return {
|
|
41
|
+
kind: 'file',
|
|
42
|
+
name: 'office',
|
|
43
|
+
// It can only read what it can parse, so the content decides, whatever the name says.
|
|
44
|
+
accepts: f => isOffice(f.source, f.sniffedType),
|
|
45
|
+
async process(file, sink, context) {
|
|
46
|
+
const run = options ?? context[contained_1.RUN_OPTIONS] ?? {};
|
|
47
|
+
const r = await (0, engine_1.disarmAtDepth)(file.source, sink, run, context.depth, context.deadline);
|
|
48
|
+
// A container in another package scores these by the weights this run used.
|
|
49
|
+
for (const f of [...r.before.findings, ...(r.after?.findings ?? [])])
|
|
50
|
+
f.weight = (0, score_1.weightOf)(f, run);
|
|
51
|
+
const findings = r.before.findings;
|
|
52
|
+
// The sink holds the rewritten file, also when nothing was removed.
|
|
53
|
+
if (r.status === 'clean')
|
|
54
|
+
return { result: 'passed', findings, outputFindings: r.after?.findings };
|
|
55
|
+
if (r.status === 'defused')
|
|
56
|
+
return { result: 'scrubbed', findings, outputFindings: r.after?.findings };
|
|
57
|
+
return { result: 'removed', findings };
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
package/dist/ppt.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type CfbEntry, CompoundFile } from './cfb';
|
|
2
|
+
import { type RunContext } from './context';
|
|
3
|
+
/**
|
|
4
|
+
* Inspects a PowerPoint 97 to 2003 presentation. PowerPoint finds its objects through the persist directory that the
|
|
5
|
+
* current edit names. This reads every record at the top level of the stream as well as every object the persist
|
|
6
|
+
* directories name, so an object that one route hides is still read.
|
|
7
|
+
*/
|
|
8
|
+
export declare function powerPointRules(ctx: RunContext, cfb: CompoundFile, stream: CfbEntry): Promise<void>;
|
package/dist/ppt.js
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.powerPointRules = powerPointRules;
|
|
37
|
+
const zlib = __importStar(require("node:zlib"));
|
|
38
|
+
const cfb_1 = require("./cfb");
|
|
39
|
+
const contained_1 = require("./contained");
|
|
40
|
+
const context_1 = require("./context");
|
|
41
|
+
const errors_1 = require("./errors");
|
|
42
|
+
const io_1 = require("./io");
|
|
43
|
+
const links_1 = require("./links");
|
|
44
|
+
const objects_1 = require("./objects");
|
|
45
|
+
const offcrypto_1 = require("./offcrypto");
|
|
46
|
+
const oleobj_1 = require("./oleobj");
|
|
47
|
+
const types_1 = require("./types");
|
|
48
|
+
/** Record types of the PowerPoint binary format (MS-PPT 2.13.24). */
|
|
49
|
+
const T = {
|
|
50
|
+
currentUserAtom: 0x0ff6,
|
|
51
|
+
userEditAtom: 0x0ff5,
|
|
52
|
+
persistDirectoryAtom: 0x1772,
|
|
53
|
+
exOleObjStg: 0x1011,
|
|
54
|
+
exOleObjAtom: 0x0fc3,
|
|
55
|
+
vbaInfoAtom: 0x0400,
|
|
56
|
+
interactiveInfo: 0x0ff2,
|
|
57
|
+
interactiveInfoAtom: 0x0ff3,
|
|
58
|
+
cString: 0x0fba,
|
|
59
|
+
exHyperlink: 0x0fd7,
|
|
60
|
+
exHyperlinkAtom: 0x0fd3,
|
|
61
|
+
cryptSession: 0x2f14,
|
|
62
|
+
};
|
|
63
|
+
/** ExVideoContainer, ExMIDIAudioContainer and ExWAVAudioLinkContainer, whose path names a file outside the presentation. */
|
|
64
|
+
const MEDIA_LINKS = new Set([0x1005, 0x100d, 0x1010]);
|
|
65
|
+
/** The CurrentUserAtom's header token of an encrypted presentation. */
|
|
66
|
+
const ENCRYPTED_TOKEN = 0xf3d1c4df;
|
|
67
|
+
const MAX_DEPTH = 64;
|
|
68
|
+
const u32 = (b, at) => (b[at] | (b[at + 1] << 8) | (b[at + 2] << 16) | (b[at + 3] << 24)) >>> 0;
|
|
69
|
+
const header = (b) => ({ version: b[0] & 0x0f, instance: (b[0] | (b[1] << 8)) >> 4, type: b[2] | (b[3] << 8), length: u32(b, 4) });
|
|
70
|
+
/**
|
|
71
|
+
* Inspects a PowerPoint 97 to 2003 presentation. PowerPoint finds its objects through the persist directory that the
|
|
72
|
+
* current edit names. This reads every record at the top level of the stream as well as every object the persist
|
|
73
|
+
* directories name, so an object that one route hides is still read.
|
|
74
|
+
*/
|
|
75
|
+
async function powerPointRules(ctx, cfb, stream) {
|
|
76
|
+
const location = stream.name;
|
|
77
|
+
let source = cfb.source(stream);
|
|
78
|
+
let reader = new io_1.Reader(source, stream.size, 16384, 8);
|
|
79
|
+
const persist = new Map();
|
|
80
|
+
const current = cfb.child(cfb.root, 'Current User');
|
|
81
|
+
if (current?.kind === 'stream') {
|
|
82
|
+
const cu = await cfb.source(current).read(0, 24);
|
|
83
|
+
if (cu.length >= 20 && header(cu).type === T.currentUserAtom) {
|
|
84
|
+
await readPersist(reader, stream.size, u32(cu, 16), persist, ctx.settings.deadline);
|
|
85
|
+
if (u32(cu, 12) === ENCRYPTED_TOKEN) {
|
|
86
|
+
source = await decryptPresentation(ctx, reader, stream, u32(cu, 16), persist);
|
|
87
|
+
reader = new io_1.Reader(source, stream.size, 16384, 8);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const storages = new Map();
|
|
92
|
+
/** ExOleObjAtom types by the persist id of the storage they name: 0 embedded, 1 linked, 2 a control. */
|
|
93
|
+
const roles = new Map();
|
|
94
|
+
const vba = new Set();
|
|
95
|
+
const visited = new Set();
|
|
96
|
+
const walk = async (start, end, depth, siblings) => {
|
|
97
|
+
for (let pos = start; pos + 8 <= end;) {
|
|
98
|
+
(0, errors_1.checkTime)(ctx.settings.deadline);
|
|
99
|
+
const h = header(await reader.read(pos, 8));
|
|
100
|
+
const bodyEnd = pos + 8 + h.length;
|
|
101
|
+
if (bodyEnd > end || visited.has(pos))
|
|
102
|
+
return;
|
|
103
|
+
visited.add(pos);
|
|
104
|
+
siblings.push({ h, offset: pos });
|
|
105
|
+
if (h.type === T.exOleObjStg)
|
|
106
|
+
storages.set(pos, { offset: pos, header: h });
|
|
107
|
+
else if (h.type === T.exOleObjAtom && h.length >= 20) {
|
|
108
|
+
const b = await reader.read(pos + 8, 20);
|
|
109
|
+
roles.set(u32(b, 16), u32(b, 4));
|
|
110
|
+
}
|
|
111
|
+
else if (h.type === T.vbaInfoAtom && h.length >= 4)
|
|
112
|
+
vba.add(u32(await reader.read(pos + 8, 4), 0));
|
|
113
|
+
if (h.version === 0x0f && depth < MAX_DEPTH) {
|
|
114
|
+
const kids = [];
|
|
115
|
+
await walk(pos + 8, bodyEnd, depth + 1, kids);
|
|
116
|
+
await containerRules(ctx, reader, h, pos, kids, location);
|
|
117
|
+
}
|
|
118
|
+
pos = bodyEnd;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
// Every record at the top level, then each persist object that does not start on a record already read.
|
|
122
|
+
await walk(0, stream.size, 0, []);
|
|
123
|
+
for (const offset of persist.values()) {
|
|
124
|
+
if (visited.has(offset) || offset + 8 > stream.size)
|
|
125
|
+
continue;
|
|
126
|
+
const h = header(await reader.read(offset, 8));
|
|
127
|
+
await walk(offset, Math.min(stream.size, offset + 8 + h.length), 0, []);
|
|
128
|
+
}
|
|
129
|
+
const byOffset = new Map([...persist].map(([id, offset]) => [offset, id]));
|
|
130
|
+
for (const s of storages.values()) {
|
|
131
|
+
const id = byOffset.get(s.offset);
|
|
132
|
+
const role = id !== undefined && vba.has(id) ? 'vba' : id !== undefined && roles.get(id) === 2 ? 'control' : 'object';
|
|
133
|
+
await storageRules(ctx, source, s, role, `${location}, offset ${s.offset}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* The PowerPoint Document stream of an encrypted presentation, decrypted (MS-PPT 2.3.7). The current edit names the
|
|
138
|
+
* persist object that holds the CryptSession10Container, whose data is an RC4 CryptoAPI EncryptionHeader. Each other
|
|
139
|
+
* persist object, its record header included, is RC4 with the key of block number its persist id. The edits and
|
|
140
|
+
* persist directories were never encrypted.
|
|
141
|
+
*/
|
|
142
|
+
async function decryptPresentation(ctx, reader, stream, edit, persist) {
|
|
143
|
+
const required = (reason) => new errors_1.StopError(types_1.OfficeCategory.Encrypted, types_1.OfficeDetail.PasswordRequired, `The presentation is encrypted: ${reason}`, { stream: stream.name, reason });
|
|
144
|
+
const u = await reader.read(edit, 40);
|
|
145
|
+
const sessionAt = u.length >= 40 && header(u).type === T.userEditAtom ? persist.get(u32(u, 36)) : undefined;
|
|
146
|
+
if (sessionAt === undefined || sessionAt + 8 > stream.size)
|
|
147
|
+
throw required('the current edit names no CryptSession10Container');
|
|
148
|
+
const h = header(await reader.read(sessionAt, 8));
|
|
149
|
+
if (h.type !== T.cryptSession)
|
|
150
|
+
throw required('the current edit names no CryptSession10Container');
|
|
151
|
+
const keyOf = (0, offcrypto_1.unlockPresentation)(ctx, await reader.read(sessionAt + 8, Math.min(h.length, 65536)), stream.name);
|
|
152
|
+
if (stream.size > ctx.settings.decompressedBytes)
|
|
153
|
+
throw new errors_1.StopError(types_1.OfficeCategory.Limit, types_1.OfficeDetail.DecompressedSize, `The encrypted stream ${stream.name} is ${stream.size} bytes`, { name: stream.name, size: stream.size, limit: ctx.settings.decompressedBytes });
|
|
154
|
+
const copy = Buffer.from(await reader.read(0, stream.size));
|
|
155
|
+
for (const [id, offset] of persist) {
|
|
156
|
+
(0, errors_1.checkTime)(ctx.settings.deadline);
|
|
157
|
+
if (offset === sessionAt || offset + 8 > copy.length)
|
|
158
|
+
continue;
|
|
159
|
+
const rc4 = keyOf(id);
|
|
160
|
+
const head = rc4.crypt(copy.subarray(offset, offset + 8));
|
|
161
|
+
const length = Math.min(copy.length - offset - 8, u32(head, 4));
|
|
162
|
+
copy.set(head, offset);
|
|
163
|
+
copy.set(rc4.crypt(copy.subarray(offset + 8, offset + 8 + length)), offset + 8);
|
|
164
|
+
}
|
|
165
|
+
return (0, io_1.bufferSource)(copy);
|
|
166
|
+
}
|
|
167
|
+
/** Reads the persist directories of the current edit and every edit before it. Newer entries win. */
|
|
168
|
+
async function readPersist(reader, size, start, persist, deadline) {
|
|
169
|
+
const seen = new Set();
|
|
170
|
+
for (let edit = start; edit > 0 && edit + 36 <= size && !seen.has(edit);) {
|
|
171
|
+
(0, errors_1.checkTime)(deadline);
|
|
172
|
+
seen.add(edit);
|
|
173
|
+
const u = await reader.read(edit, 36);
|
|
174
|
+
if (header(u).type !== T.userEditAtom)
|
|
175
|
+
return;
|
|
176
|
+
const dirAt = u32(u, 20);
|
|
177
|
+
const d = await reader.read(dirAt, 8);
|
|
178
|
+
if (d.length === 8 && header(d).type === T.persistDirectoryAtom) {
|
|
179
|
+
const body = await reader.read(dirAt + 8, Math.min(header(d).length, size - dirAt - 8));
|
|
180
|
+
for (let p = 0; p + 4 <= body.length;) {
|
|
181
|
+
const entry = u32(body, p);
|
|
182
|
+
const first = entry & 0xfffff;
|
|
183
|
+
const count = entry >>> 20;
|
|
184
|
+
p += 4;
|
|
185
|
+
for (let i = 0; i < count && p + 4 <= body.length; i++, p += 4)
|
|
186
|
+
if (!persist.has(first + i))
|
|
187
|
+
persist.set(first + i, u32(body, p));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
edit = u32(u, 16);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/** One ExOleObjStg: a VBA project, a control or an OLE object, stored as a compound file that may be compressed. */
|
|
194
|
+
async function storageRules(ctx, source, s, role, location) {
|
|
195
|
+
let inner;
|
|
196
|
+
if (s.header.instance === 1) {
|
|
197
|
+
const raw = await source.read(s.offset + 8, s.header.length);
|
|
198
|
+
const declared = raw.length >= 4 ? u32(raw, 0) : 0;
|
|
199
|
+
if (declared > ctx.settings.decompressedBytes)
|
|
200
|
+
throw new errors_1.StopError(types_1.OfficeCategory.Limit, types_1.OfficeDetail.DecompressedSize, `An object at ${location} declares ${declared} bytes`, { name: location, size: declared, limit: ctx.settings.decompressedBytes });
|
|
201
|
+
let bytes;
|
|
202
|
+
try {
|
|
203
|
+
bytes = zlib.inflateSync(raw.subarray(4), { maxOutputLength: Math.max(1, declared) });
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
unreadable(ctx, role, location, 'its compressed data does not inflate to its declared size');
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
inner = { size: async () => bytes.length, read: async (offset, length) => bytes.subarray(offset, offset + length) };
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
const start = s.offset + 8;
|
|
213
|
+
const length = s.header.length;
|
|
214
|
+
inner = { size: async () => length, read: (offset, n) => source.read(start + offset, Math.max(0, Math.min(n, length - offset))) };
|
|
215
|
+
}
|
|
216
|
+
const head = await inner.read(0, 8);
|
|
217
|
+
let cfb;
|
|
218
|
+
try {
|
|
219
|
+
if (head.length < 8 || head[0] !== 0xd0 || head[1] !== 0xcf)
|
|
220
|
+
throw new errors_1.StopError(types_1.OfficeCategory.Container, types_1.OfficeDetail.BadHeader, 'not a compound file');
|
|
221
|
+
cfb = await cfb_1.CompoundFile.open(inner, ctx.settings, ctx.settings.deadline);
|
|
222
|
+
}
|
|
223
|
+
catch (e) {
|
|
224
|
+
if (!(e instanceof errors_1.StopError) || e.category !== types_1.OfficeCategory.Container)
|
|
225
|
+
throw e;
|
|
226
|
+
unreadable(ctx, role, location, `its storage does not read: ${e.message}`);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
(0, objects_1.nestedNotes)(ctx, cfb, location);
|
|
230
|
+
if (role === 'control')
|
|
231
|
+
ctx.report(types_1.OfficeCategory.Control, types_1.OfficeDetail.ActiveX, location, { storage: location, ...(cfb.root.clsid === '00000000-0000-0000-0000-000000000000' ? {} : { clsid: cfb.root.clsid }) });
|
|
232
|
+
// An object's storage goes to the plugins as the compound file it is. The rewrite cannot change the presentation's
|
|
233
|
+
// records, so only a plugin that passes the object keeps it in the output.
|
|
234
|
+
const size = await inner.size();
|
|
235
|
+
const decision = role === 'object' ? await (0, contained_1.decideContained)(ctx, { location, chunks: () => (0, io_1.chunksOf)(inner, size) }) : undefined;
|
|
236
|
+
await (0, objects_1.compoundRules)(ctx, cfb, location, role === 'object', undefined, decision);
|
|
237
|
+
}
|
|
238
|
+
function unreadable(ctx, role, location, reason) {
|
|
239
|
+
if (role === 'vba')
|
|
240
|
+
ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.VbaProject, location, { storage: location, reason });
|
|
241
|
+
else if (role === 'control')
|
|
242
|
+
ctx.report(types_1.OfficeCategory.Control, types_1.OfficeDetail.ActiveX, location, { storage: location, reason });
|
|
243
|
+
else
|
|
244
|
+
ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.OleObject, location, { storage: location, reason });
|
|
245
|
+
}
|
|
246
|
+
/** The rules of one container record, from its children: actions, hyperlinks and linked media. */
|
|
247
|
+
async function containerRules(ctx, reader, h, offset, kids, stream) {
|
|
248
|
+
const location = `${stream}, offset ${offset}`;
|
|
249
|
+
const strings = async () => {
|
|
250
|
+
const out = new Map();
|
|
251
|
+
for (const k of kids)
|
|
252
|
+
if (k.h.type === T.cString && !out.has(k.h.instance))
|
|
253
|
+
out.set(k.h.instance, (0, oleobj_1.utf16)(await reader.read(k.offset + 8, Math.min(k.h.length, 65536))));
|
|
254
|
+
return out;
|
|
255
|
+
};
|
|
256
|
+
if (h.type === T.interactiveInfo) {
|
|
257
|
+
const atom = kids.find(k => k.h.type === T.interactiveInfoAtom && k.h.length >= 16);
|
|
258
|
+
if (!atom)
|
|
259
|
+
return;
|
|
260
|
+
const action = (await reader.read(atom.offset + 8 + 8, 1))[0];
|
|
261
|
+
const name = (await strings()).get(2) ?? '';
|
|
262
|
+
const data = { trigger: h.instance === 1 ? 'mouse over' : 'click', ...(name ? { action: (0, context_1.clip)(name) } : {}) };
|
|
263
|
+
if (action === 2)
|
|
264
|
+
ctx.report(types_1.OfficeCategory.Action, types_1.OfficeDetail.RunProgram, location, data);
|
|
265
|
+
else if (action === 1)
|
|
266
|
+
ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.Link, location, data);
|
|
267
|
+
else if (action === 5)
|
|
268
|
+
ctx.report(types_1.OfficeCategory.Action, types_1.OfficeDetail.OleVerb, location, data);
|
|
269
|
+
}
|
|
270
|
+
else if (h.type === T.exHyperlink) {
|
|
271
|
+
const s = await strings();
|
|
272
|
+
const target = s.get(1) ?? '';
|
|
273
|
+
if (target.trim() && !(0, links_1.isFragment)(target))
|
|
274
|
+
ctx.link(target, location, { source: 'PowerPoint hyperlink' });
|
|
275
|
+
}
|
|
276
|
+
else if (MEDIA_LINKS.has(h.type)) {
|
|
277
|
+
const path = (await strings()).get(0) ?? '';
|
|
278
|
+
if (path.trim())
|
|
279
|
+
ctx.external(types_1.OfficeDetail.Other, path, location, { source: 'linked sound or movie' }, 'linked media');
|
|
280
|
+
}
|
|
281
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CfbEntry, CompoundFile } from './cfb';
|
|
2
|
+
import { type CfbTree } from './cfb-writer';
|
|
3
|
+
import type { RunState } from './context';
|
|
4
|
+
import { CompoundPlan } from './plan';
|
|
5
|
+
import type { ByteSink } from './types';
|
|
6
|
+
/**
|
|
7
|
+
* Writes a compound file without the streams and storages the plan drops. A stream a plugin replaced takes the bytes
|
|
8
|
+
* the plugin wrote, and an object's storage a plugin replaced takes the storages and streams of the compound file the
|
|
9
|
+
* plugin wrote. Every other stream is copied as it is. Sectors no stream holds, the rest of each stream's last sector
|
|
10
|
+
* and anything after the last sector are not written.
|
|
11
|
+
*/
|
|
12
|
+
export declare function writeCompound(run: RunState, cfb: CompoundFile, plan: CompoundPlan, sink: ByteSink): Promise<void>;
|
|
13
|
+
/** A storage and everything under it, as the root of a compound file of its own. */
|
|
14
|
+
export declare function subtree(cfb: CompoundFile, storage: CfbEntry): CfbTree;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.writeCompound = writeCompound;
|
|
4
|
+
exports.subtree = subtree;
|
|
5
|
+
const cfb_writer_1 = require("./cfb-writer");
|
|
6
|
+
const plan_1 = require("./plan");
|
|
7
|
+
/** The nodes of a storage's subtree, without what the plan drops, with what plugins returned in place of the rest. */
|
|
8
|
+
function nodes(cfb, storage, plan, opened) {
|
|
9
|
+
const out = [];
|
|
10
|
+
for (const c of storage.children) {
|
|
11
|
+
if (plan.drop.has(c))
|
|
12
|
+
continue;
|
|
13
|
+
const replaced = plan.replace.get(c);
|
|
14
|
+
if (c.kind === 'stream') {
|
|
15
|
+
if (!replaced) {
|
|
16
|
+
out.push({ kind: 'stream', name: c.name, size: c.size, source: cfb.source(c) });
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
const source = replaced.source();
|
|
20
|
+
opened.push(source);
|
|
21
|
+
out.push({ kind: 'stream', name: c.name, size: replaced.length, source });
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const graft = plan.grafts.get(c);
|
|
25
|
+
if (graft)
|
|
26
|
+
out.push({ kind: 'storage', name: c.name, clsid: graft.root.clsid === '00000000-0000-0000-0000-000000000000' ? '' : graft.root.clsid, children: nodes(graft, graft.root, plan, opened) });
|
|
27
|
+
else
|
|
28
|
+
out.push({ kind: 'storage', name: c.name, clsid: c.clsid === '00000000-0000-0000-0000-000000000000' ? '' : c.clsid, children: nodes(cfb, c, plan, opened) });
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Writes a compound file without the streams and storages the plan drops. A stream a plugin replaced takes the bytes
|
|
34
|
+
* the plugin wrote, and an object's storage a plugin replaced takes the storages and streams of the compound file the
|
|
35
|
+
* plugin wrote. Every other stream is copied as it is. Sectors no stream holds, the rest of each stream's last sector
|
|
36
|
+
* and anything after the last sector are not written.
|
|
37
|
+
*/
|
|
38
|
+
async function writeCompound(run, cfb, plan, sink) {
|
|
39
|
+
const clsid = cfb.root.clsid === '00000000-0000-0000-0000-000000000000' ? '' : cfb.root.clsid;
|
|
40
|
+
const opened = [];
|
|
41
|
+
try {
|
|
42
|
+
await (0, cfb_writer_1.writeCfb)({ clsid, children: nodes(cfb, cfb.root, plan, opened) }, sink, run.deadline);
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
for (const s of opened)
|
|
46
|
+
await s.close?.();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** A storage and everything under it, as the root of a compound file of its own. */
|
|
50
|
+
function subtree(cfb, storage) {
|
|
51
|
+
return { clsid: storage.clsid === '00000000-0000-0000-0000-000000000000' ? '' : storage.clsid, children: nodes(cfb, storage, new plan_1.CompoundPlan(), []) };
|
|
52
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { RunState } from './context';
|
|
2
|
+
import type { RtfCut } from './plan';
|
|
3
|
+
import type { ByteSink, ByteSource } from './types';
|
|
4
|
+
/**
|
|
5
|
+
* Writes an RTF file without the ranges the cuts name: the groups of the destinations a strip finding names, and any
|
|
6
|
+
* bytes after the group that closes the document. A cut can put text in place of what it removes, such as an object's
|
|
7
|
+
* result group in place of the object. Everything else keeps its bytes.
|
|
8
|
+
*/
|
|
9
|
+
export declare function writeRtf(run: RunState, source: ByteSource, size: number, cuts: RtfCut[], sink: ByteSink): Promise<void>;
|