@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/README.md
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
# office-defuse
|
|
2
2
|
|
|
3
|
-
office-defuse reads Word, Excel, PowerPoint
|
|
3
|
+
office-defuse reads Word, Excel, PowerPoint and Visio files, in their ZIP formats and in the compound file formats before them, OpenDocument packages, Outlook messages, Publisher and Project files, and RTF. It reports VBA, WordBasic and Excel 4.0 macros, LibreOffice Basic and scripts, DDE, fields and events that run code, templates and objects loaded from outside the file, embedded OLE objects, Equation Editor objects, attachments, ActiveX controls and unsafe links. It scores the risk from 0 to 100.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
It also writes defused copies. A copy leaves out what each strip finding names, and it reaches you only after it passes an inspection of its own.
|
|
6
6
|
|
|
7
|
-
It has no runtime dependencies and keeps memory bounded. It reads
|
|
7
|
+
It has no runtime dependencies and keeps memory bounded. It reads ZIP archives, compound files and XML strictly, and refuses anything two readers could take in different ways. RTF it reads the way Word does, quirks included.
|
|
8
8
|
|
|
9
9
|
```
|
|
10
10
|
npm install @patrickdk77/office-defuse
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
Node
|
|
13
|
+
Node 18 or later.
|
|
14
14
|
|
|
15
15
|
## Quick start
|
|
16
16
|
|
|
@@ -21,14 +21,27 @@ const report = await inspectOffice(bytes);
|
|
|
21
21
|
report.status; // 'clean', 'strippable' or 'rejected'
|
|
22
22
|
report.risk; // 'NONE', 'LOW', 'MEDIUM', 'HIGH', 'CRITICAL' or 'UNKNOWN'
|
|
23
23
|
report.score; // 0 to 100, null when the file could not be inspected
|
|
24
|
-
report.format; // 'docx', '
|
|
24
|
+
report.format; // 'docx', 'xls', 'msg', 'rtf', ... or '' when not recognized
|
|
25
25
|
report.mimeType; // the MIME type of that format, or ''
|
|
26
26
|
report.findings; // [{ category, detail, description, action, location, data }]
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
ES modules work too: `import { inspectOffice } from '@patrickdk77/office-defuse'`.
|
|
30
30
|
|
|
31
|
-
`strippable` means at least one finding has the action `strip`, content a defused copy would leave out. `rejected` means a finding has the action `reject`, or the run stopped before it read the whole
|
|
31
|
+
`strippable` means at least one finding has the action `strip`, content a defused copy would leave out, or a file plugin cleaned a file inside. `rejected` means a finding has the action `reject`, or the run stopped before it read the whole file.
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
const { disarmOffice } = require('@patrickdk77/office-defuse');
|
|
35
|
+
|
|
36
|
+
const result = await disarmOffice(bytes);
|
|
37
|
+
result.status; // 'clean', 'defused' or 'rejected'
|
|
38
|
+
result.before; // the inspection of the upload
|
|
39
|
+
result.after; // the inspection of the output, absent when rejected
|
|
40
|
+
result.removed; // the findings whose content the output leaves out
|
|
41
|
+
result.bytes; // the output, absent when rejected
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`clean` means the upload had nothing to remove and the output is a rewrite of it. See [Disarm](#disarm).
|
|
32
45
|
|
|
33
46
|
### Files and S3
|
|
34
47
|
|
|
@@ -48,40 +61,194 @@ await inspectOfficeSource(source);
|
|
|
48
61
|
|
|
49
62
|
`inspectOfficeSource` calls the source's `close()`, if it has one, when the run ends. A `read` must return exactly the bytes asked for, or fewer only at the end of the source. Any other length fails the run with an error whose `code` is `EIO`. An error the source throws comes back unchanged, instead of a verdict on the file.
|
|
50
63
|
|
|
64
|
+
`disarmOfficeSource(source, sink, options)` writes the output to a sink with `write(chunk)`, `close()` and an optional `abort(error)`. `bufferSink`, `fileSink` and `writableSink`, which wraps a Node stream, are included. The run copies the upload once, into memory up to `memoryThreshold` and into a temporary file past it, and reads only that copy, so a source that changes during the run cannot change the output. Nothing reaches the sink until the output has passed its check. A copy to the sink that fails calls `abort()` instead of `close()`, and `fileSink` then deletes the partial file.
|
|
65
|
+
|
|
66
|
+
```js
|
|
67
|
+
const { disarmOfficeSource, fileSource, fileSink } = require('@patrickdk77/office-defuse');
|
|
68
|
+
|
|
69
|
+
const result = await disarmOfficeSource(fileSource('upload.docx'), fileSink('clean.docx'));
|
|
70
|
+
```
|
|
71
|
+
|
|
51
72
|
## Formats
|
|
52
73
|
|
|
53
74
|
The format comes from the content, never from the file name.
|
|
54
75
|
|
|
55
|
-
| Family |
|
|
56
|
-
|
|
57
|
-
| Word | .docx .docm .dotx .dotm |
|
|
58
|
-
| Excel | .xlsx .xlsm .xltx .xltm .xlam .xlsb |
|
|
59
|
-
| PowerPoint | .pptx .pptm .potx .potm .ppsx .ppsm .ppam .sldx .sldm |
|
|
60
|
-
| Visio | .vsdx .vsdm .vstx .vstm .vssx .vssm |
|
|
61
|
-
|
|
|
62
|
-
|
|
63
|
-
|
|
76
|
+
| Family | ZIP formats | Compound file and RTF formats | Flat XML formats |
|
|
77
|
+
|---|---|---|---|
|
|
78
|
+
| Word | .docx .docm .dotx .dotm | .doc .dot from Word 6.0 to 2003, and .rtf | Flat OPC .xml |
|
|
79
|
+
| Excel | .xlsx .xlsm .xltx .xltm .xlam .xlsb | .xls .xlt .xla from Excel 5.0 to 2003 | Flat OPC .xml |
|
|
80
|
+
| PowerPoint | .pptx .pptm .potx .potm .ppsx .ppsm .ppam .sldx .sldm | .ppt from PowerPoint 97 to 2003 | Flat OPC .xml |
|
|
81
|
+
| Visio | .vsdx .vsdm .vstx .vstm .vssx .vssm | .vsd from Visio 2000 to 2010 | |
|
|
82
|
+
| XPS | .xps .oxps | | |
|
|
83
|
+
| OpenDocument | .odt .ott .ods .ots .odp .otp .odg .otg .odf .odm .odb | | .fodt .fods .fodp .fodg |
|
|
84
|
+
| Outlook | | .msg .oft | |
|
|
85
|
+
| Publisher | | .pub | |
|
|
86
|
+
| Project | | .mpp | |
|
|
87
|
+
|
|
88
|
+
`[Content_Types].xml` marks an Office Open XML package. The package relationship of type `officeDocument`, transitional or strict, or Visio's `document`, names the main part, and its content type gives the format. An XPS package names its FixedDocumentSequence with a `fixedrepresentation` relationship. XPS and OpenXPS share their content types, so the relationship's namespace tells them apart. The `mimetype` entry marks an OpenDocument package, or the manifest's entry for `/` when there is no mimetype entry.
|
|
64
89
|
|
|
65
90
|
A ZIP with both markers is rejected as PACKAGE/AMBIGUOUS_FORMAT, since Office could read one and LibreOffice the other. So is an OOXML package with two main parts, and an ODF package whose mimetype and manifest name different formats. Anything else is PACKAGE/UNRECOGNIZED.
|
|
66
91
|
|
|
67
92
|
A .xlsb gets the package checks. Its sheets are binary records, and the formulas in them go unread. Its external link parts are binary too, and office-defuse reads their link records for DDE links. A .odb gets every ODF check, and the database under `database/` goes unread.
|
|
68
93
|
|
|
94
|
+
A file that starts with the compound file signature is a compound file, MS-CFB. The streams and storages at its root name the format: `WordDocument` for Word, `Workbook` or `Book` for Excel, `PowerPoint Document`, `VisioDocument`, `__properties_version1.0` for an Outlook message, a `Quill` storage or Publisher's class id beside a `Contents` stream for Publisher, and a `Props` stream beside a storage named by a number for Project. Word's FIB says whether a document is a template, and Excel's records whether a workbook is a template or an add-in. An Outlook template has a class id of its own. A PowerPoint 97 to 2003 template or show reports its format as .ppt, and a Visio stencil or template as .vsd.
|
|
95
|
+
|
|
96
|
+
A compound file with two of these is PACKAGE/AMBIGUOUS_FORMAT. One with none is PACKAGE/UNRECOGNIZED, and so is a `WordDocument` stream that holds no Word 6.0 to 2003 FIB. A PowerPoint 4.0 file, a bare OLE object and a `vbaProject.bin` on its own all land there.
|
|
97
|
+
|
|
98
|
+
An encrypted .docx, .xlsx or .pptx is a compound file holding `EncryptionInfo` and `EncryptedPackage`. See [Encrypted files](#encrypted-files).
|
|
99
|
+
|
|
100
|
+
A file that starts as XML, after any byte order mark and white space, is a flat file. A root `office:document` makes it flat OpenDocument, whose `office:mimetype` gives the format. A root `pkg:package` makes it Flat OPC, the one-file form Word, Excel and PowerPoint save as XML, and its format is `xml` whatever its main part is. Any other root is PACKAGE/UNRECOGNIZED, and so is a flat OpenDocument media type outside the table. A file that is XML and a ZIP archive too is PACKAGE/AMBIGUOUS_FORMAT.
|
|
101
|
+
|
|
102
|
+
A file that starts with `{\rt` is RTF, as Word takes it. A compound file or RTF file in which the ZIP reader also finds an archive is PACKAGE/AMBIGUOUS_FORMAT. A file with the compound file signature whose compound file does not read, but whose ZIP archive does, is that ZIP archive with LEADING_BYTES.
|
|
103
|
+
|
|
69
104
|
## What it reports
|
|
70
105
|
|
|
71
|
-
|
|
106
|
+
In a ZIP package:
|
|
107
|
+
|
|
108
|
+
- VBA projects, Excel 4.0 macro sheets, and OpenDocument `Basic/`, `Scripts/` and `Dialogs/` folders. These go by content type, relationship type or folder name, and a folder counts when it holds a file that is not empty. office-defuse parses no OLE part of a package, such as `vbaProject.bin`.
|
|
72
109
|
- OpenDocument script event listeners that name a script URL or macro. Those that fire as the document opens or is created get a finding of their own. LibreOffice ignores a listener whose URL and macro name are both empty, and so does office-defuse. Visio ShapeSheet cells that call RUNADDON, RUNADDONWARGS, CALLTHIS, RUNMACRO, OPENFILE or DOOLEVERB.
|
|
73
110
|
- Links and actions that run macros: `vnd.sun.star.script:` and `macro:` targets, PowerPoint's `ppaction://macro`, Excel drawing shapes with a `macro` attribute, and form controls with an `x:FmlaMacro`. Ribbon callbacks such as `onLoad` and `onAction`. OpenDocument menu, toolbar and key settings under `Configurations2/`. LibreOffice writes an empty `Configurations2/accelerator/current.xml` into the documents it saves, and an empty file counts for nothing.
|
|
111
|
+
- Excel defined names that run a macro sheet: Auto_Open and Auto_Activate as MACRO/EVENT_ON_OPEN, Auto_Close and Auto_Deactivate as MACRO/EVENT. A name counts when it starts with one of these, as `Auto_Open2` does.
|
|
74
112
|
- PowerPoint and Impress actions that run a program or an object verb.
|
|
75
113
|
- DDE: Word `DDE` and `DDEAUTO` fields, Excel `ddeLink` parts and the .xlsb record for them, Excel formulas in `application|topic!item` form, OpenDocument DDE connections and `DDE()` formulas. No amount of white space before a field's name hides it, and a formula is checked whole however long it runs.
|
|
76
114
|
- Word fields that run a macro, `MACROBUTTON`, and fields whose type another field builds. That is the QUOTE trick that hides `DDEAUTO`.
|
|
115
|
+
- Formula functions that reach outside the file when Excel or LibreOffice calculates them: WEBSERVICE and IMAGE fetch a URL, as EXTERNAL/FORMULA, and RTD, CALL, REGISTER, REGISTER.ID and EXEC run code outside the file, as MACRO/FORMULA. A prefix such as `_xlfn.` or `COM.MICROSOFT.` names the same function. A name in quotes is text, and a macro sheet's formulas are its macros, which its own finding covers. The rewrite removes the formula and the cell keeps its cached value.
|
|
116
|
+
- The sources an Excel data connection in `connections.xml` loads from, each as EXTERNAL/DATA_CONNECTION: an ODC file or offline cube, a connection string and its command, a web query's URL, or a text file. The rewrite empties each source and keeps the connection, which query tables and pivot caches name.
|
|
117
|
+
- Where an OpenDocument form sends its fields, as ACTION/SUBMIT_FORM, which the rewrite removes, as pdf-defuse removes a form submit. Word's hyperlink base in `docProps/app.xml` gets the link checks.
|
|
77
118
|
- References that load from outside the file on their own: attached and OpenDocument templates, linked OLE objects and packages, images, frames, included files and linked sections, external workbooks, `meta:auto-reload`, and any other external relationship. A target on the machine itself is EXTERNAL/LOCAL_FILE. An empty OpenDocument reference is the document itself and names no file, so it goes unreported. A target counts as local only if its percent-decoded form is local too, because a CVE-2017-0199 sample writes its `script:` scheme as `%73cript:`.
|
|
78
119
|
- Hyperlinks, with pdf-defuse's checks of the target: the scheme, file and network paths, data URLs, credentials before the host, IP hosts, punycode and mixed-script hosts, and percent-encoded hosts. A link to a place inside the document goes unreported, and so does an empty link, which is the document itself.
|
|
120
|
+
- A safe link whose label is an address of another site than the one it opens is LINK/TEXT_MISMATCH. A label counts as an address when it is a web URL, a mail address, or a host with or without a path that starts with `www.` or ends in a known top-level domain. Text that only mentions an address names no site, and neither do Excel function names such as `BETA.DIST` and file names such as `moon.htm`, which look like hosts. Sites compare as in pdf-defuse: a subdomain or parent of the label's site matches, and `evil.co.uk` is not under `mybank.co.uk`. The labels are a Word hyperlink's text and tooltip, a HYPERLINK field's result and `\o` tooltip, a DrawingML run's text and a link's tooltip, an Excel hyperlink's display name and tooltip, an OpenDocument link's text and title, and the characters of an XPS Glyphs element. The rewrite removes the link and keeps its text.
|
|
79
121
|
- Embedded OLE objects and packages, OLE files nothing declares as an object, Java applets, plug-ins and ActiveX controls.
|
|
80
122
|
- Package structure: parts without a content type, parts no relationship reaches, files the ODF manifest does not list, an irregular ODF layout, and names a package gives twice.
|
|
81
123
|
- Document type declarations in XML parts, and references to external entities. See [Document types and entities](#document-types-and-entities).
|
|
82
124
|
|
|
125
|
+
- In XPS, the markup names the parts it uses: a document's, page's or resource dictionary's `Source`, a font's `FontUri`, an image brush's `ImageSource` and a `ContextColor` profile. Each counts as reaching its part. One outside the package is EXTERNAL, with IMAGE for an image, INCLUDE for a `Source` and OTHER for the rest, and the element that names it goes. A `FixedPage.NavigateUri` or `OutlineTarget` with a scheme or a network path gets the link checks, and a relative one moves inside the document. Pictures go to the file plugins, and obfuscated fonts, which are part of the format, do not.
|
|
126
|
+
|
|
83
127
|
The OOXML rules match elements by namespace URI and local name, compared ignoring case, so a prefix bound to the Word namespace reads as Word whatever it is called. The OpenDocument rules read `urn:oasis:names:tc:<any>:xmlns:<name>:1.<any>` as version 1.0 of that namespace, which is what LibreOffice does. Every branch of `mc:AlternateContent` gets read.
|
|
84
128
|
|
|
129
|
+
A flat OpenDocument file gets the OpenDocument rules over its one part, which findings name `office:document`. It has no entries, so every relative reference names a file beside the document and loads from outside it, as EXTERNAL/LOCAL_FILE. A chart's `xlink:href=".."` names the document that holds the chart, and a `vnd.sun.star.Package:` URL the document's own storage, so neither counts. The pictures and OLE objects it holds as base64 in `office:binary-data` go to the file plugins, and an OLE object no plugin keeps is EMBEDDED/OLE_OBJECT.
|
|
130
|
+
|
|
131
|
+
A Flat OPC package is unpacked into a package of its parts, each with its `pkg:contentType`, and read with the OOXML rules. A part's XML keeps the namespaces it inherits from `pkg:package`. Two parts with one name are CONTAINER/DUPLICATE_NAME, and a part with no name is PACKAGE/UNRECOGNIZED.
|
|
132
|
+
|
|
133
|
+
In a compound file, the first two rules run over every storage of the tree, and the others over the streams of the format:
|
|
134
|
+
|
|
135
|
+
- VBA projects: a `VBA` storage with a `dir` stream, or a `_VBA_PROJECT_CUR` or `Macros` storage with something in it.
|
|
136
|
+
- Embedded objects, by the streams an object's storage holds, under `ObjectPool` or anywhere else: OLE objects with their class id, ProgID and user type, the file a Packager object holds, Equation Editor 3.0 objects by class id or ProgID, ActiveX controls, and linked objects with the target their moniker names. An object's storage is part of the tree, so a macro or an object inside an object is found.
|
|
137
|
+
- Word: WordBasic macros in Word 6.0 and 95 files. The fields of the text, with the rules a .docx gets, and the attached template. A Word 6.0 or 95 file saved fast is read in the order its piece table gives, and any other one from fcMin to fcMac.
|
|
138
|
+
- Excel: Excel 4.0 macro sheets, by sheet type or by the sheet's own BOF record, and the Auto_ names above, built in or defined. The functions above in a worksheet's cell formulas, by their index in Excel's function table or by the name a newer function is called by, such as `_xlfn.WEBSERVICE`. External workbooks, with the paths Excel encodes written out, DDE and OLE links, hyperlinks, shapes and buttons that run a macro, and the `Ctls` stream that holds ActiveX controls.
|
|
139
|
+
- PowerPoint: actions that run a macro, a program or an object verb, hyperlinks, and linked sounds and movies. PowerPoint keeps the storages of objects, controls and the VBA project in its document stream, often compressed. office-defuse inflates them and gives them the rules above.
|
|
140
|
+
- Outlook: each attachment, as EMBEDDED/ATTACHMENT with its name, MIME type and size, and each attachment by reference with its path. An attached message's own attachments are read, 16 messages deep. The RTF body, decompressed as MS-OXRTFCP says, gets the RTF rules, and its findings name `RTF body` as their attachment. A body that does not decompress, or whose CRC does not match, is PACKAGE/UNREADABLE_BODY. Each embedded message's bodies get the same reading.
|
|
141
|
+
- Visio 2000 to 2010: ShapeSheet cells whose formulas call RUNADDON, RUNADDONWARGS, CALLTHIS, RUNMACRO, OPENFILE or DOOLEVERB, as in a .vsdx. A call in EventDblClick, EventXFMod, EventDrop or EventMultiDrop is MACRO/EVENT, and one in any other cell MACRO/LINK, with the section, the cell and the formula. Microsoft has not published the format, so office-defuse reads it as libvisio, Apache POI and OLEToy describe it, and finds every call to these functions in their test files. Visio stores a formula as tokens. The finding writes the formula out when it holds only constants, operators and the call, and gives the function's name alone otherwise. office-defuse then searches each stream for the token of such a call outside the formulas it read. That covers chunk types whose cells it cannot place, and the finding names the cell `unknown`. Decompressed streams count toward `limits.decompressedBytes` and `limits.totalDecompressedBytes`. A pointer, chunk, cell block or formula that does not read stops the run as PACKAGE/UNREADABLE. The VisioDocument stream of Visio 5.0 and earlier goes unread.
|
|
142
|
+
- Publisher and Project: the VBA projects and objects. Nothing in their own streams is read.
|
|
143
|
+
|
|
144
|
+
In RTF:
|
|
145
|
+
|
|
146
|
+
- Objects in `\objdata`. office-defuse decodes the OLE 1.0 object, the file a Packager object holds, and the storage of an OLE 2 object, which gets the compound file rules. An object linked with `\objlink` or `\objautlink`, or of the class OLE2Link that CVE-2017-0199 uses, is EXTERNAL/OBJECT. `\objocx` marks an ActiveX control. `\objupdate` is ACTION/OBJECT_UPDATE, since it starts the object's program as the document opens.
|
|
147
|
+
- Fields, with the rules a .docx gets, the `\*\template` destination, and shape hyperlinks.
|
|
148
|
+
- Text after the group that closes the document, as TRAILING_BYTES. White space and NULs there are what writers leave, and count for nothing.
|
|
149
|
+
|
|
150
|
+
### Encrypted files
|
|
151
|
+
|
|
152
|
+
office-defuse decrypts these, all from MS-OFFCRYPTO:
|
|
153
|
+
|
|
154
|
+
- An OOXML package encrypted with Standard or Agile encryption, Agile in CBC or CFB chaining.
|
|
155
|
+
- An .xls workbook encrypted with RC4 or RC4 CryptoAPI, or obfuscated with XOR, Excel 5.0 and 95's only kind.
|
|
156
|
+
- A Word 97 to 2003 document encrypted with RC4 or RC4 CryptoAPI, or obfuscated with XOR. Its WordDocument, table and Data streams are decrypted where they are read.
|
|
157
|
+
- A PowerPoint 97 to 2003 presentation encrypted with RC4 CryptoAPI, one persist object at a time, each with the key of its persist id.
|
|
158
|
+
|
|
159
|
+
`password` takes one password or a list, since the files in one upload can have different passwords. office-defuse tries each one in order, each once, and then the password the application tries on its own: VelvetSweatshop for Excel and OOXML, and `/01Hannes Ruescher/01` for PowerPoint, so such a file opens without asking. Word has none. A password meant for another file never keeps the default from opening this one. The decrypted file gets every check of its format. It is read in place. Agile encryption decrypts in 4,096-byte segments and Standard in 16-byte blocks, so only the parts office-defuse reads get decrypted. A PowerPoint stream is decrypted whole into memory, up to `limits.decompressedBytes`.
|
|
160
|
+
|
|
161
|
+
A binary file stays encrypted in the output, since the rewrite copies its streams as they are. The output then still holds ENCRYPTED/DEFAULT_PASSWORD or USER_PASSWORD, which strip, so disarm rejects it unless an override makes them `info`. An encrypted OOXML package comes out decrypted.
|
|
162
|
+
|
|
163
|
+
| Finding | Action | Weight | When |
|
|
164
|
+
|---|---|---|---|
|
|
165
|
+
| ENCRYPTED/DEFAULT_PASSWORD | `strip` | 10 | An application's default password opened the file. Anyone can open it, so the encryption hides its content from a scanner and from no one else |
|
|
166
|
+
| ENCRYPTED/USER_PASSWORD | `strip` | 0 | The password the caller gave opened the file |
|
|
167
|
+
| ENCRYPTED/RC4_40, RC4_128, RC4_OTHER, AES_128, AES_192, AES_256, XOR_OBFUSCATION | `info` | 0 | The cipher and key size |
|
|
168
|
+
| ENCRYPTED/PASSWORD_REQUIRED | `reject` | 0 | No password at hand opens the file, or an Agile spin count is above 10,000,000. Every encrypted Word 6.0 or 95 document, which office-defuse does not decrypt |
|
|
169
|
+
| ENCRYPTED/UNSUPPORTED_ALGORITHM | `reject` | 0 | A cipher, chaining or hash outside the list below |
|
|
170
|
+
|
|
171
|
+
AES-128, AES-192 and AES-256 in CBC or ECB mode, SHA-1, SHA-256, SHA-384, SHA-512 and MD5 go to Node's crypto module, and no other algorithm name from a file reaches it. Agile's CFB chaining, CFB with an 8-bit window, is built on AES-ECB. RC4 and XOR obfuscation are office-defuse's own code, since OpenSSL 3 keeps RC4 in its legacy provider.
|
|
172
|
+
|
|
173
|
+
Pass passwords in code, or to the command-line tool in a file or `OFFICE_DEFUSE_PASSWORD`. Never pass one as an argument, where other users can read it.
|
|
174
|
+
|
|
175
|
+
## Disarm
|
|
176
|
+
|
|
177
|
+
`disarmOffice` and `disarmOfficeSource` inspect the file, write a copy without the content each strip finding names, and inspect the copy. If that check reports any finding that is not `info`, the copy is thrown away. The upload is rejected with PROCESSING/VERIFICATION_FAILED, whose `data.reasons` names what the check found, and it keeps its score. A copy whose format differs from the upload's fails the same way. Every file that is not rejected is rewritten, clean ones included, so the output holds only what the run read.
|
|
178
|
+
|
|
179
|
+
The check reads the output with the caller's overrides and limits, without `fileSize`, `timeMs` and `nestingDepth`, and within the deadline the upload's run has. Disarm reads the file twice, once to inspect it and once to check the output, so it takes about twice as long as an inspection.
|
|
180
|
+
|
|
181
|
+
### ZIP packages
|
|
182
|
+
|
|
183
|
+
- The archive is new. Data before the first entry, between entries and after the end goes, and so do extra fields and data descriptors. An entry nothing changes keeps its compressed bytes and its timestamp.
|
|
184
|
+
- A part goes with its own relationships, every relationship that targets it, its override in `[Content_Types].xml`, and the parts that only it reached, so a VBA project takes its signature and `vbaData.xml` with it.
|
|
185
|
+
- A relationship a strip finding names goes, such as an external template, object, image or frame, or a hyperlink to an unsafe target.
|
|
186
|
+
- An element that names a relationship that went goes too, so no element points at nothing. A `w:hyperlink` loses its tags and keeps its text. A SpreadsheetML list that loses its last item goes. An external link part whose link went goes, and the workbook's reference to it with it.
|
|
187
|
+
- A part loses its root element only by going whole. Edits happen in place, and everything the rewrite does not touch keeps its characters, in the part's own encoding, UTF-8 or UTF-16, behind its own byte order mark.
|
|
188
|
+
- A Word field a strip finding names loses its begin, separator and end marks and its code, nested fields included, and keeps its result. A `w:fldSimple` keeps the runs inside it.
|
|
189
|
+
- An Excel 4.0 macro sheet becomes an empty worksheet of the same name, with the worksheet content type and relationship type, so the indexes of the sheets after it stay right. A defined name that runs a macro goes. So does a formula that calls DDE, and its cell keeps its cached value.
|
|
190
|
+
- A drawing shape loses its `macro` attribute, a ribbon control its callbacks, and a PowerPoint shape an action that runs a program, a macro or an object verb. A Visio cell that calls code goes.
|
|
191
|
+
- The main part keeps its content type, so a `.docm` without its VBA project is still a `.docm`.
|
|
192
|
+
- In an OpenDocument package, `mimetype` comes first, stored, without an extra field, and holds the media type without white space. A package without one gets one, and a package without a manifest gets a manifest listing its entries. `Basic/`, `Scripts/`, `Dialogs/` and `Configurations2/` go whole. An entry goes with its manifest entry and with every element whose `xlink:href` names it. A link `text:a` or `draw:a` keeps what it holds. A form control loses its link, and a cell loses a formula that calls DDE. An embedded OLE object goes and the preview image beside it stays.
|
|
193
|
+
- A document type declaration with an internal subset goes when XML/INTERNAL_SUBSET strips it. Each reference to an internal entity is written out as its text, markup included, and each start tag whose attributes the declarations changed or added to is written out whole, namespace declarations included. The part then reads as the inspection read it. References to external entities go when XML/EXTERNAL_ENTITY strips them. If that finding is overridden to `info` while the subset would go, the references still need their declarations, so the declaration stays and the check of the output reports it.
|
|
194
|
+
- An element that came from entity text has the offsets of the reference, so the rewrite writes that one reference out, without the element.
|
|
195
|
+
- An encrypted package is written decrypted. office-defuse does not encrypt.
|
|
196
|
+
- No PDF header starts in the first 1024 bytes of the copy, where PDF readers look for one. Entries that went, and parts rewritten to another size, move what follows forward, so a stored PDF, an entry name or a local header from deep in the upload can land there. A copied entry that would put a header there is deflated again. Any other entry that would waits until later entries have filled those bytes, up to 16 entries. What still lands there fails the output check, and the upload is rejected.
|
|
197
|
+
|
|
198
|
+
### Flat files
|
|
199
|
+
|
|
200
|
+
A flat OpenDocument file is rewritten in place, as a package's XML part is. An element goes or loses an attribute where a strip finding names it, and base64 that a plugin rewrote replaces the file's own.
|
|
201
|
+
|
|
202
|
+
A Flat OPC package is rewritten as the package of its parts is, then written back as one XML file in UTF-8. It starts with an XML declaration and the prolog's `mso-application` processing instructions, which tell Windows which application opens the file. Each part keeps its name, its content type and its other attributes, such as `pkg:compression`. XML parts go in `pkg:xmlData` and the rest as base64 in `pkg:binaryData`, in lines of 76 characters. Anything else in the prolog, such as a document type declaration, is left out.
|
|
203
|
+
|
|
204
|
+
### Compound files
|
|
205
|
+
|
|
206
|
+
The rewrite writes a new compound file, version 3 with 512-byte sectors, without the streams and storages a strip finding names: VBA project storages, the storages of embedded objects, linked objects and ActiveX controls, Excel's `Ctls` stream, and Outlook attachments. A message's property stream counts its attachments, and the count drops by the ones that went.
|
|
207
|
+
|
|
208
|
+
The FAT comes right after the header, so no stream lands in the first 1024 bytes, where PDF readers look for a header. When a stream ends with a ZIP end record, as an embedded package does, free sectors of zeros follow the last stream until the record is more than 65,557 bytes from the end of the file, past where ZIP readers look.
|
|
209
|
+
|
|
210
|
+
An Outlook message's RTF body with a strip finding is rewritten the way an RTF file is, and stored back uncompressed, which MS-OXRTFCP allows. A body that does not decompress, or one with a `reject` finding inside, goes, and the message keeps its plain text body. The property stream entry of a body that went goes too, and that of a rewritten body takes its new size. Every other stream is copied unchanged. Sectors no stream holds, the rest of each stream's last sector and data past the last sector are not written.
|
|
211
|
+
|
|
212
|
+
Content inside a stream stays as it was. That covers Word's fields, template and WordBasic macros, Excel's records for macro sheets, defined names, links, hyperlinks and buttons, every record of a PowerPoint presentation along with the objects stored in its records, the ShapeSheet cells of a Visio drawing, and the RC4 encryption of a workbook. A finding there is still in the output, so the output fails its check and the upload is rejected.
|
|
213
|
+
|
|
214
|
+
### RTF
|
|
215
|
+
|
|
216
|
+
The rewrite copies the file without the groups of the destinations a strip finding names. A field loses its `\fldinst` group and keeps its result. The `\*\template` destination goes, and so does a shape hyperlink's `\hlsrc`. An object's group goes, and the content of its `\result` group takes its place as a plain group, so the picture Word shows stays. When ACTION/OBJECT_UPDATE strips and nothing else does, `\objupdate` goes on its own. Text after the group that closes the document goes.
|
|
217
|
+
|
|
218
|
+
## File plugins
|
|
219
|
+
|
|
220
|
+
`filePlugins` takes contained-file plugins with pdf-defuse's shapes. office-defuse shows them each file it finds inside the file:
|
|
221
|
+
|
|
222
|
+
- In an OOXML package, its OLE parts, which are VBA projects, embedded objects, ActiveX binaries and any part whose bytes are an OLE file, its embedded packages, and every other part that is a whole file in a format of its own, such as the images under `media/` and a thumbnail. The binary parts of the format itself go to no plugin: the records of a binary workbook, printer settings, obfuscated fonts and VBA project signatures.
|
|
223
|
+
- In an OpenDocument package, each entry outside `META-INF/` and the code and settings folders that is not read as XML, such as an OLE object, a picture or the thumbnail.
|
|
224
|
+
- In an Outlook message, its RTF body, decompressed, and its HTML body. A plugin's copy of the RTF body is stored uncompressed.
|
|
225
|
+
- In a compound file the rewrite writes, each embedded object. A Packager object shows the file it holds, and any other object its storage, written out as a compound file of its own. Outlook attachments held in a stream go too, whatever their type. In a PowerPoint presentation, the storages of its objects go as the compound files they are.
|
|
226
|
+
- In RTF, an embedded object's native data, or the file a Packager object holds, and each picture that is a whole file, PNG, JPEG or EMF.
|
|
227
|
+
- In a flat OpenDocument file, each picture and OLE object held as base64 in `office:binary-data`. A Flat OPC package's parts go as a package's parts do.
|
|
228
|
+
|
|
229
|
+
A file comes as a `ContainedFile`. `name` is the part or entry name, a Packager file's name, or an object's type. `declaredType` is a part's content type or a picture's type. `sniffedType` comes from the first bytes, with the MIME types pdf-defuse gives and `application/rtf`. `location` says where the file sits, `depth` is its depth in the upload, and `source` reads its bytes. The first plugin whose `accepts` returns true decides.
|
|
230
|
+
|
|
231
|
+
| The plugin returns | The file | Finding |
|
|
232
|
+
|---|---|---|
|
|
233
|
+
| `passed` | stays, as the plugin wrote it when it wrote anything | EMBEDDED/PLUGIN_PASSED, `info` |
|
|
234
|
+
| `scrubbed`, with bytes written | is replaced by those bytes | EMBEDDED/PLUGIN_SCRUBBED, `info` |
|
|
235
|
+
| `removed` | goes, with every reference to it | EMBEDDED/PLUGIN_REMOVED, `strip` |
|
|
236
|
+
| `scrubbed` with nothing written, or an error | goes, with every reference to it | EMBEDDED/PLUGIN_FAILED, `strip` |
|
|
237
|
+
|
|
238
|
+
A file goes whatever the overrides say about these findings. A file no plugin accepts gets the rule it gets without plugins, such as EMBEDDED/OLE_OBJECT, and a picture, which no rule names, stays. When a plugin keeps a file, the findings that name the file for being there, such as EMBEDDED/PACKAGE or MACRO/VBA_PROJECT for `vbaProject.bin`, turn to `info` and leave the score, as pdf-defuse leaves out an attached file a plugin kept. Other rules still decide for themselves, so an Equation Editor object a plugin passes goes anyway, since EMBEDDED/EQUATION strips it. A plugin's output for an object's storage must be a compound file, whose storages and streams then take the storage's place. Where the rewrite cannot change the container, as for an object stored in a PowerPoint record, the output keeps the file as it arrived, so a plugin's `scrubbed` or `removed` there fails the output check.
|
|
239
|
+
|
|
240
|
+
The findings a plugin returns go into the report with their `location` after the file's own and with `attachment` naming the file. A `reject` inside the file becomes `strip`, since the file goes and the file holding it stays. Each scores at its `weight`. The upload's score leaves out PLUGIN_PASSED and PLUGIN_SCRUBBED, and the output's counts them. The check of the output does not ask the plugins again. It matches each file by the MD5 of its bytes against what the plugins kept, and reports the `outputFindings` the plugin gave for it.
|
|
241
|
+
|
|
242
|
+
The plugin context has `depth`, the file's depth in the upload, and `deadline`, which every file of the upload shares. An error with `code: 'DEFUSE_LIMIT'` or `code: 'DEFUSE_IO'` from a plugin stops the whole upload. At depth 0, a limit becomes LIMIT/TIME or LIMIT/NESTING_DEPTH, and an I/O error is thrown as its cause.
|
|
243
|
+
|
|
244
|
+
`officePlugin(options)` defuses Office and OpenDocument files inside other files. It accepts a file by its content alone: a compound file, RTF, a ZIP archive with `[Content_Types].xml`, a `mimetype` entry or `META-INF/manifest.xml`, or XML whose root is a flat OpenDocument document or a Flat OPC package. It runs disarm at the context's depth with the context's deadline. Without `options` it takes the options of the office-defuse run that found the file, or none when another package found it. A clean file comes back `passed`, a defused one `scrubbed`, both with the rewritten bytes, and a rejected one `removed`. The findings it returns carry their `weight`.
|
|
245
|
+
|
|
246
|
+
```js
|
|
247
|
+
const { disarmOffice, officePlugin } = require('@patrickdk77/office-defuse');
|
|
248
|
+
|
|
249
|
+
await disarmOffice(bytes, { filePlugins: [officePlugin()] });
|
|
250
|
+
```
|
|
251
|
+
|
|
85
252
|
## The containers
|
|
86
253
|
|
|
87
254
|
The ZIP reader starts from the end record and the central directory. It checks each local header against the central directory before it reads any content. It refuses these, and stops the run:
|
|
@@ -95,8 +262,8 @@ The ZIP reader starts from the end record and the central directory. It checks e
|
|
|
95
262
|
| OUT_OF_RANGE | A central directory or local header outside the file or its entries, or a 64-bit value larger than a JavaScript number holds exactly |
|
|
96
263
|
| OVERLAP | Two entries that share bytes, or an entry that runs into the central directory |
|
|
97
264
|
| HEADER_MISMATCH | A local header that is missing, or whose name, method, encryption, data descriptor or UTF-8 flags, CRC or sizes differ from the central directory, and a data descriptor that differs |
|
|
98
|
-
| DUPLICATE_NAME | Two names equal after percent-decoding, Unicode
|
|
99
|
-
| UNSAFE_NAME | An empty name, a name over 1024 bytes, a leading `/`, a drive letter, a backslash, a control character,
|
|
265
|
+
| DUPLICATE_NAME | Two names equal after percent-decoding, Unicode NFKC and full case folding, and without the dots and spaces Windows drops from the end of each segment. A sharp s (U+00DF) meets SS, a ligature meets its letters, and `a.xml.` meets `a.xml` |
|
|
266
|
+
| UNSAFE_NAME | An empty name, a name over 1024 bytes, a leading `/`, a drive letter, a backslash, a control character, a segment of only dots and spaces, such as `..`, `.. ` or `...`, which Windows reads as `..` or as empty, or a directory entry with content |
|
|
100
267
|
| NAME_ENCODING | A non-ASCII name without the UTF-8 flag, invalid UTF-8, or an Info-ZIP Unicode path field that names another entry |
|
|
101
268
|
| ENCRYPTED_ENTRY | Any of the encryption flags |
|
|
102
269
|
| UNSUPPORTED_METHOD | Any compression method but stored and deflate |
|
|
@@ -104,7 +271,7 @@ The ZIP reader starts from the end record and the central directory. It checks e
|
|
|
104
271
|
| BAD_EXTRA_FIELD | An extra field block that does not split exactly into records, or a record id given twice |
|
|
105
272
|
| DATA_ERROR | Data that does not inflate to its declared size and CRC, deflate data that is corrupt, ends early or leaves compressed bytes unused, or a stored entry with two different sizes |
|
|
106
273
|
|
|
107
|
-
It reports
|
|
274
|
+
It reports four more without stopping. LEADING_BYTES is data before the first entry. HIDDEN_DATA is bytes between entries, or before the central directory, that no entry covers. HIDDEN_ENTRY is a local file header the central directory does not list. It can sit in those uncovered bytes, or inside the data of a stored entry that has a data descriptor, since a reader that walks the file finds the end of such an entry only by looking for the next signature. A streaming unzipper extracts a hidden entry as a file, and a reader of the central directory never sees it. TRAILING_BYTES is bytes after the end record that its comment does not cover. Each is `strip`. The rewrite writes no data descriptors, so a reader that walks the copy finds only the listed entries.
|
|
108
275
|
|
|
109
276
|
The reader inflates every entry once, start to end, and checks it against its CRC and declared size. Inflation stops as soon as the output passes the declared size.
|
|
110
277
|
|
|
@@ -121,6 +288,39 @@ The `Basic/`, `Scripts/`, `Dialogs/` and `Configurations2/` folders of an OpenDo
|
|
|
121
288
|
|
|
122
289
|
PowerPoint and Excel write VML drawings that are not well-formed XML. They put `<![if gte mso 9]>` and `<![endif]>` around content, and Excel 2007 leaves `<br>` unclosed in a button's text. office-defuse reads a part of the VML drawing type in that dialect. It skips the markers, so the content on both sides of them reaches the rules, and it takes `<br>` as an empty element. Apart from those two, it reads the part as strictly as any other.
|
|
123
290
|
|
|
291
|
+
### Compound files
|
|
292
|
+
|
|
293
|
+
The compound file reader follows MS-CFB, and where MS-CFB leaves room, it accepts what olefile and LibreOffice accept. It reads the header, the FAT through the DIFAT, the directory, the mini FAT and the mini stream, and records which stream owns each sector, so no sector serves two. It follows a stream's chain only as far as the stream's size needs, as olefile and LibreOffice do, so a chain that runs on past that does no harm. It refuses these, and stops the run:
|
|
294
|
+
|
|
295
|
+
| Detail | Refused |
|
|
296
|
+
|---|---|
|
|
297
|
+
| BAD_HEADER | A file shorter than its header, a version and sector size other than 3 with 512 bytes or 4 with 4,096, mini sectors other than 64 bytes, a mini stream cutoff other than 4,096, a byte order other than little-endian, FAT or DIFAT sector counts that disagree with the sectors listed, a mini FAT too short for the mini stream, or an empty directory |
|
|
298
|
+
| OUT_OF_RANGE | A sector or a stream outside the file, a directory entry that names one past the directory, or a stream size larger than a JavaScript number holds exactly |
|
|
299
|
+
| OVERLAP | A sector or mini sector that two streams take, or a stream and the FAT, DIFAT, directory or mini FAT |
|
|
300
|
+
| CHAIN_LOOP | A FAT, DIFAT or mini FAT chain that comes back to a sector it took already |
|
|
301
|
+
| BROKEN_CHAIN | A chain that runs into a free or reserved sector before its stream ends, or a DIFAT chain that does not end after its last sector |
|
|
302
|
+
| DATA_ERROR | A chain that ends before its stream does |
|
|
303
|
+
| DIRECTORY_LOOP | A directory entry the tree reaches twice |
|
|
304
|
+
| BAD_ENTRY | An entry in the tree of an unknown or unused type, a root entry that is not the first, a name length or terminator that does not fit, a NUL in a name, or a stream with children |
|
|
305
|
+
| UNSAFE_NAME | A name holding `/`, `\`, `:` or `!`, which MS-CFB forbids |
|
|
306
|
+
| DUPLICATE_NAME | Two entries of one storage whose names are equal ignoring case |
|
|
307
|
+
|
|
308
|
+
It reports two more without stopping, each `strip`. HIDDEN_DATA counts the bytes that no stream covers and that are not zero: sectors no chain takes, the rest of each stream's last sector or mini sector, mini sectors no stream takes, directory entries the tree does not reach, and the padding of a version 4 header. Zeros are padding and count for nothing. TRAILING_BYTES is data past the last sector the FAT describes. HIDDEN_DATA is common in real files. LibreOffice, for one, leaves an old copy of a directory sector in a free sector when it saves.
|
|
309
|
+
|
|
310
|
+
`limits.entries` counts directory entries, `limits.decompressedBytes` bounds the size of one stream, and `limits.totalDecompressedBytes` the sizes of all streams together, the way they count ZIP entries. The reader holds the FAT, the mini FAT, the directory and the owner of each sector, and reads streams in ranges as the rules ask for them.
|
|
311
|
+
|
|
312
|
+
### RTF
|
|
313
|
+
|
|
314
|
+
The RTF reader takes the file in 256 KiB pieces. It holds the destinations open at the point it has reached, the short text it collects, such as a class name or a template path, and the data of each object it decodes. It reads the way Word reads:
|
|
315
|
+
|
|
316
|
+
- A control word's name and its number each end after 250 characters.
|
|
317
|
+
- A destination that `\*` marks and that office-defuse does not know is skipped with its group, and an unknown control word is ignored. Every destination of RTF 1.9.1 is known, so a decoy group such as `{\listtable 4142}` inside an object's data adds no hex digits.
|
|
318
|
+
- In an object's data, anything but hex digits drops out, a digit left unpaired before `\'hh` is dropped, and `\bin` data goes in as its bytes.
|
|
319
|
+
- Inside an object's data, a group that `\*` marks and that opens with a control word that is no destination, such as `{\*\par574 0}`, still holds hex digits for rtfobj, and Word opens files that hide an object's data that way. When the data reads differently with and without those groups, both readings get the object rules, and the findings from the second have `reading` in their data.
|
|
320
|
+
- A file can end inside groups, and the reader closes them. Text after the group that closes the document is still read, as rtfobj reads it.
|
|
321
|
+
|
|
322
|
+
`limits.rtfDepth`, 256 by default, bounds group nesting and the destinations open at once. Past it the run stops with LIMIT/RTF_DEPTH. One object's decoded data counts against `limits.decompressedBytes`, and all of them together against `limits.totalDecompressedBytes`.
|
|
323
|
+
|
|
124
324
|
### Document types and entities
|
|
125
325
|
|
|
126
326
|
office-defuse never loads an external DTD and never fetches an external entity. A document type declaration does not stop the run.
|
|
@@ -143,11 +343,11 @@ Three limits bound what a document type can cost: entity references nested 16 de
|
|
|
143
343
|
|
|
144
344
|
### When a run stops
|
|
145
345
|
|
|
146
|
-
A run that stops rejects the file with a null score and the UNKNOWN level, whatever `actionOverrides` say about the finding that stopped it. The
|
|
346
|
+
A run that stops rejects the file with a null score and the UNKNOWN level, whatever `actionOverrides` say about the finding that stopped it. The container refusals, a limit, an unrecognized or ambiguous format, an encrypted file that no password at hand opens or whose algorithm office-defuse does not decrypt, and an unexpected error, PACKAGE/UNREADABLE, all stop a run. The findings made before the stop stay in the report. A time or nesting limit that a plugin or a nested run reaches stops the upload's run too.
|
|
147
347
|
|
|
148
348
|
## Findings
|
|
149
349
|
|
|
150
|
-
Every finding has a `category` and a `detail`, plain strings such as `MACRO` and `VBA_PROJECT`, and a `description` that is safe to show the uploader. A description that quotes a value from the file cuts it to 60 characters and shows control and format characters as `?`. `data` keeps a value from the file up to 200 characters, or 2,000 for a target, and `location` names the part, the relationship or the
|
|
350
|
+
Every finding has a `category` and a `detail`, plain strings such as `MACRO` and `VBA_PROJECT`, and a `description` that is safe to show the uploader. A description that quotes a value from the file cuts it to 60 characters and shows control and format characters as `?`. `data` keeps a value from the file up to 200 characters, or 2,000 for a target, and `location` names the part, the relationship, the folder, the stream or storage, or the RTF object's offset.
|
|
151
351
|
|
|
152
352
|
```js
|
|
153
353
|
{ category: 'EXTERNAL', detail: 'TEMPLATE', description: 'The document loads its template from https://templates.example.net/report.dotm', action: 'strip', location: 'word/_rels/settings.xml.rels, rIdTpl', data: { target: '...', type: '...' } }
|
|
@@ -165,7 +365,7 @@ await inspectOffice(bytes, { actionOverrides: [{ category: 'MACRO', action: 'rej
|
|
|
165
365
|
|
|
166
366
|
The method is pdf-defuse's. Each kind of finding has a weight: a program run by a click is 80, VBA is 70, a safe link is 0. The highest weight counts in full, every other kind adds a quarter of its weight, and repeats of one kind count once.
|
|
167
367
|
|
|
168
|
-
Three combinations add more. Code here means macros or DDE. Code or a run-program action together with a template, object or include from outside adds 20. Code with an embedded object or ActiveX control adds 15. Code with leading, hidden or trailing
|
|
368
|
+
Three combinations add more. Code here means macros or DDE. Code or a run-program action together with a template, object or include from outside adds 20. Code with an embedded object or ActiveX control adds 15. Code with leading, hidden or trailing container data adds 10. The total is capped at 100.
|
|
169
369
|
|
|
170
370
|
The bands are NONE at 0, LOW from 1, MEDIUM from 25, HIGH from 50 and CRITICAL from 80. `scoreWeights` and `scoreBands` change them. A finding of a kind office-defuse does not make, such as one from another package, scores at its `weight`, or 0 without one.
|
|
171
371
|
|
|
@@ -174,19 +374,25 @@ The bands are NONE at 0, LOW from 1, MEDIUM from 25, HIGH from 50 and CRITICAL f
|
|
|
174
374
|
| Option | Default | Purpose |
|
|
175
375
|
|---|---|---|
|
|
176
376
|
| `limits.fileSize` | none | Bytes in the upload, checked before any read |
|
|
177
|
-
| `limits.entries` | 10,000 | Entries in the central directory |
|
|
178
|
-
| `limits.decompressedBytes` | 512 MiB | The declared size of any one entry |
|
|
179
|
-
| `limits.totalDecompressedBytes` | 2 GiB | The declared sizes of all entries together |
|
|
377
|
+
| `limits.entries` | 10,000 | Entries in the central directory, or in a compound file's directory |
|
|
378
|
+
| `limits.decompressedBytes` | 512 MiB | The declared size of any one entry or stream, one RTF object's decoded data, or one compressed PowerPoint storage |
|
|
379
|
+
| `limits.totalDecompressedBytes` | 2 GiB | The declared sizes of all entries, or of all streams, together, and all RTF objects' decoded data |
|
|
180
380
|
| `limits.xmlDepth` | 256 | Element nesting in one XML part |
|
|
181
381
|
| `limits.xmlTokenSize` | 16 Mi | Characters in one tag, comment, processing instruction or CDATA section. A document type declaration counts as one. Text goes through in pieces and has no limit |
|
|
182
382
|
| `limits.xmlEntityDepth` | 16 | Entity references nested inside entity text |
|
|
183
383
|
| `limits.xmlEntityExpansions` | 100,000 | Entity references expanded, and attribute defaults added, in the whole run |
|
|
184
384
|
| `limits.xmlEntitySize` | 16 Mi | Characters of entity text and attribute defaults expanded in the whole run |
|
|
185
|
-
| `limits.
|
|
385
|
+
| `limits.rtfDepth` | 256 | Group nesting in an RTF file, and destinations open at once |
|
|
386
|
+
| `limits.nestingDepth` | none | The deepest a file may sit in the upload. The upload is at 0 and a file inside it at 1, whatever the formats in between |
|
|
387
|
+
| `limits.timeMs` | none | The whole run, files inside included |
|
|
388
|
+
| `password` | none | One password or a list, tried in order on an encrypted OOXML package or .xls workbook, before VelvetSweatshop |
|
|
186
389
|
| `actionOverrides` | none | Change the action for a category, or for a category and detail |
|
|
187
390
|
| `scoreWeights`, `scoreBands` | built in | Change the scoring |
|
|
391
|
+
| `filePlugins` | none | Plugins that decide about the files inside the file. See [File plugins](#file-plugins) |
|
|
392
|
+
| `tempDir` | `os.tmpdir()` | Where temporary files go |
|
|
393
|
+
| `memoryThreshold` | 8 MiB | Disarm keeps its copy of the upload, its output and each file it shows a plugin in memory up to this size, and in a temporary file past it. The copies plugins return share this much memory while they wait for the rewrite |
|
|
188
394
|
|
|
189
|
-
The
|
|
395
|
+
The nine limits with a default bound the memory or work any input can cost, so they apply unless you set them. `Infinity` turns one off. A limit that is reached stops the run with a LIMIT finding.
|
|
190
396
|
|
|
191
397
|
Besides one XML token, the stack of open elements and the declarations of the part's internal subset, office-defuse holds the central directory's table of entries, which parts each part's relationships reach, and, while it reads a relationships part, the Ids that part has given so far, about 100 bytes each, so it can find one given twice. Content types, manifest paths and object references that name no entry of the archive are not kept. Everything else streams.
|
|
192
398
|
|
|
@@ -194,20 +400,54 @@ Besides one XML token, the stack of open elements and the declarations of the pa
|
|
|
194
400
|
|
|
195
401
|
office-defuse is one of a family of packages that each handle one kind of file, beside [pdf-defuse](https://github.com/patrickdk77/pdf-defuse). It declares the family's shared types with the same shapes as pdf-defuse: `DefuseFinding`, `ByteSource`, `ByteSink`, `PluginContext`, `ContainedFile`, `ContainedFileResult` and `ContainedFilePlugin`. It does not depend on pdf-defuse, and TypeScript matches the types by shape.
|
|
196
402
|
|
|
197
|
-
A finding's `category` and `detail` are plain strings, and `weight`, when set, says how much it adds to a score. The time
|
|
403
|
+
A finding's `category` and `detail` are plain strings, and `weight`, when set, says how much it adds to a score. The time and nesting limits' errors carry `code: 'DEFUSE_LIMIT'` and `limit: 'time'` or `limit: 'nesting'`, and an I/O error inside a nested run carries `code: 'DEFUSE_IO'` with the error as its `cause`. Every package of the family passes these up the tree of files. At depth 0, office-defuse turns a limit into LIMIT/TIME or LIMIT/NESTING_DEPTH and throws the cause of an I/O error.
|
|
404
|
+
|
|
405
|
+
`@patrickdk77/file-defuse` is the family's router, one contained-file plugin that every package gets in its `filePlugins`. office-defuse hands it the files it finds inside, with their depth counted from the upload and the upload's deadline, and the router can pass an Office file inside any other file to `officePlugin`.
|
|
406
|
+
|
|
407
|
+
## Command line
|
|
408
|
+
|
|
409
|
+
```
|
|
410
|
+
office-defuse inspect <file> [options]
|
|
411
|
+
office-defuse defuse <in> <out> [options]
|
|
412
|
+
|
|
413
|
+
--password-file <path> read passwords from a file, one per line; OFFICE_DEFUSE_PASSWORD holds one
|
|
414
|
+
--plugin <module> load file plugins from a file or an installed package, repeatable
|
|
415
|
+
--config <module> load a whole options object, repeatable; the other options apply on top
|
|
416
|
+
--defuse-embedded-office defuse Office files inside the file with office-defuse and keep them
|
|
417
|
+
--limit <name>=<value> fileSize, entries, decompressedBytes, totalDecompressedBytes, xmlDepth,
|
|
418
|
+
xmlTokenSize, xmlEntityDepth, xmlEntityExpansions, xmlEntitySize, rtfDepth,
|
|
419
|
+
nestingDepth, timeMs; sizes accept kb, mb, gb
|
|
420
|
+
--json print the result as JSON
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
Use `-` for stdin or stdout. The tool copies stdin to a temporary file in `tempDir`, which a `--config` module can set. The copy stops one byte past `limits.fileSize`, and the run then rejects the upload without reading the rest. The copy counts toward `limits.timeMs`. `inspect` prints its report to stdout. `defuse` does too, unless the file itself goes to stdout, and then the report goes to stderr. `defuse` refuses an output that is its input under any name, because a failed write deletes the partial output.
|
|
424
|
+
|
|
425
|
+
`--password-file` reads one password per line. The tool drops a carriage return at the end of a line and skips blank lines. Spaces belong to the password. `OFFICE_DEFUSE_PASSWORD` holds one password, and a line break in it is part of that password. `--password-file` replaces it, and either one replaces a `password` that a `--config` module sets. `--password` is a usage error.
|
|
426
|
+
|
|
427
|
+
The text report shows each control character as an escape such as `\u001b`. The JSON report escapes DEL and the C1 range too, which JSON itself does not require. A part name, a field code or a link from the file can hold an escape sequence that rewrites what the terminal shows, the verdict included.
|
|
428
|
+
|
|
429
|
+
`fileSize`, `decompressedBytes` and `totalDecompressedBytes` take sizes. The other limits take whole numbers. Any other value is a usage error.
|
|
430
|
+
|
|
431
|
+
A `--plugin` module exports `plugins`, an array, or `plugin`, a single plugin, each with `kind: 'file'`. Either can also sit on the default export. ES modules, CommonJS files and installed packages all load, the same way pdf-defuse's tool loads them. A `--config` module's default export is the options object. Several `--config` modules combine. Their plugin lists join in order and their limits merge, and for any other option the later module wins. The flags apply on top of every module, wherever they sit on the command line.
|
|
432
|
+
|
|
433
|
+
Exit codes are 0 for clean, 1 for defused or strippable, 2 for rejected and 3 for a usage or I/O error. An output or a report that could not be written is an I/O error. An unknown option shows in the error without the text after its `=`.
|
|
198
434
|
|
|
199
435
|
## What it does not check
|
|
200
436
|
|
|
201
|
-
- The
|
|
202
|
-
-
|
|
203
|
-
-
|
|
204
|
-
-
|
|
205
|
-
-
|
|
437
|
+
- The OLE parts of a ZIP package, such as `vbaProject.bin`, the objects inside `oleObject*.bin`, and ActiveX binaries. office-defuse hands them to the file plugins and reads none of them itself.
|
|
438
|
+
- VBA source. A VBA project is reported whole, and its modules go unread.
|
|
439
|
+
- Encrypted Word 6.0 and 95 documents. They are rejected. In a Word document encrypted with RC4 CryptoAPI, the OLE objects that Word keeps in the Data stream rather than in ObjectPool. The pictures of an encrypted presentation.
|
|
440
|
+
- Formulas in .xlsb sheets, and in an Excel 5.0 or 95 workbook. A DDE link that a formula uses is found in the workbook's link records.
|
|
441
|
+
- Hyperlinks in a Visio .vsd, ShapeSheet cells in a drawing from Visio 5.0 or earlier, and anything in Publisher and Project files but VBA projects and objects.
|
|
442
|
+
- An Outlook message's plain text body. What an attached file holds is for the file plugins, and so is the HTML body.
|
|
443
|
+
- Images, fonts and other media, and the inside of an embedded Office package such as a chart's workbook. These go to the file plugins, and `officePlugin` reads an embedded package.
|
|
444
|
+
- Data connections in an .xlsb, which are binary.
|
|
445
|
+
- Word 2003 XML and SpreadsheetML 2003, single XML files with their own vocabulary. They are PACKAGE/UNRECOGNIZED.
|
|
206
446
|
- Bugs in an application's parsers that ordinary content can trigger.
|
|
207
447
|
|
|
208
448
|
## Development
|
|
209
449
|
|
|
210
|
-
Development needs Node 22.12 or later. mocha 12 requires it, and the CommonJS test build loads chai 6, an ES module, with `require()`, which Node 22 supports only from 22.12. The pre-commit hook needs Node 22.22.1 or later for lint-staged. The tests also run `python3` and `unzip` to read the ZIP writer's output. The published package has no runtime dependencies and runs on
|
|
450
|
+
Development needs Node 22.12 or later. mocha 12 requires it, and the CommonJS test build loads chai 6, an ES module, with `require()`, which Node 22 supports only from 22.12. The pre-commit hook needs Node 22.22.1 or later for lint-staged. The tests also run `python3` and `unzip` to read the ZIP writer's output. The published package has no runtime dependencies and runs on Node 18 or later.
|
|
211
451
|
|
|
212
452
|
```sh
|
|
213
453
|
npm install --ignore-scripts
|
|
@@ -222,9 +462,11 @@ npm run format
|
|
|
222
462
|
|
|
223
463
|
`npx husky` activates the pre-commit hook. A plain `npm install` does the same through the `prepare` script. The hook runs Biome on the staged files, applying its safe fixes, then runs `npm audit`. The commit stops if either one fails.
|
|
224
464
|
|
|
225
|
-
The fixtures under `test/fixtures` are committed. `python3 -I scripts/make-fixtures.py` rebuilds them, and needs LibreOffice's `soffice`, Python 3 and Info-ZIP's `zip`. It converts flat OpenDocument sources with LibreOffice, adds macros, fields, relationships, objects and controls with Python's `zipfile`, and builds the hostile archives byte by byte.
|
|
465
|
+
The fixtures under `test/fixtures` are committed. `python3 -I scripts/make-fixtures.py` rebuilds them, and needs LibreOffice's `soffice`, Python 3 and Info-ZIP's `zip`. It converts flat OpenDocument sources with LibreOffice, which also saves .doc, .xls, .ppt and .rtf versions of some of them. It adds macros, fields, relationships, objects and controls with Python's `zipfile`, and builds the hostile archives byte by byte. Compound files, RTF, Outlook messages and encrypted files come from its own compound file writer, AES and RC4. Two runs write the same bytes: the script sets `SAL_RAND_REPEATABLE` for LibreOffice, and fixes the timestamps and random ids LibreOffice writes.
|
|
466
|
+
|
|
467
|
+
`test/fixtures/media` holds packages and RTF files with pictures and with the binary parts of the formats themselves, for the file plugins. `python3 -I scripts/make-fixtures.py media` writes them with Python alone.
|
|
226
468
|
|
|
227
|
-
`test/fixtures/cases` holds one small
|
|
469
|
+
`test/fixtures/cases` holds one small file for each behavior a run over a corpus of real Office files turned up, written from scratch by `python3 -I scripts/make-fixtures.py cases`, which needs only Python. No corpus file, file name or path is in the repository, macro and OLE payloads are inert markers, and every address uses a reserved name. `manifest.json` gives each file's issue, what office-defuse reports for it, and what the build before the fix it documents reported. A test checks every entry, and that the manifest and the folder list the same files.
|
|
228
470
|
|
|
229
471
|
## License
|
|
230
472
|
|
package/dist/biff.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { CfbEntry, CompoundFile } from './cfb';
|
|
2
|
+
/** Record types this reader acts on (MS-XLS 2.3). */
|
|
3
|
+
export declare const R: {
|
|
4
|
+
bof: number;
|
|
5
|
+
eof: number;
|
|
6
|
+
filePass: number;
|
|
7
|
+
continue: number;
|
|
8
|
+
lbl: number;
|
|
9
|
+
externName: number;
|
|
10
|
+
boundSheet: number;
|
|
11
|
+
template: number;
|
|
12
|
+
addIn: number;
|
|
13
|
+
supBook: number;
|
|
14
|
+
externSheet: number;
|
|
15
|
+
obj: number;
|
|
16
|
+
hlink: number;
|
|
17
|
+
};
|
|
18
|
+
export interface BiffRecord {
|
|
19
|
+
type: number;
|
|
20
|
+
/** Where the record header starts in the stream. */
|
|
21
|
+
offset: number;
|
|
22
|
+
body: Uint8Array;
|
|
23
|
+
}
|
|
24
|
+
/** A sheet as BoundSheet8 (or BIFF5's BOUNDSHEET) lists it. */
|
|
25
|
+
export interface Sheet {
|
|
26
|
+
position: number;
|
|
27
|
+
/** 0 visible, 1 hidden, 2 very hidden. */
|
|
28
|
+
state: number;
|
|
29
|
+
/** 0 worksheet or dialog, 1 macro sheet, 2 chart, 6 VBA module. */
|
|
30
|
+
type: number;
|
|
31
|
+
name: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Reads the records of a workbook stream one at a time, from `start` to the end of the stream. A record's body may
|
|
35
|
+
* run past the stream's end only by being cut short, which stops the reading as Excel stops.
|
|
36
|
+
*/
|
|
37
|
+
export declare class RecordReader {
|
|
38
|
+
readonly stream: CfbEntry;
|
|
39
|
+
private readonly deadline?;
|
|
40
|
+
private readonly reader;
|
|
41
|
+
private pos;
|
|
42
|
+
constructor(cfb: CompoundFile, stream: CfbEntry, start: number, deadline?: number | undefined);
|
|
43
|
+
next(): Promise<BiffRecord | undefined>;
|
|
44
|
+
}
|
|
45
|
+
/** BIFF8 writes strings with a flag byte: 8-bit characters, or 16-bit ones when its low bit is set. */
|
|
46
|
+
export declare function xlString(b: Uint8Array, at: number, chars: number): {
|
|
47
|
+
text: string;
|
|
48
|
+
next: number;
|
|
49
|
+
};
|
|
50
|
+
/** A BoundSheet record, in BIFF8 or BIFF5. */
|
|
51
|
+
export declare function parseBoundSheet(body: Uint8Array, biff8: boolean): Sheet;
|
|
52
|
+
/** A defined name, in BIFF8 or BIFF5, with a built-in name spelled out. */
|
|
53
|
+
export declare function parseLbl(body: Uint8Array, biff8: boolean): {
|
|
54
|
+
name: string;
|
|
55
|
+
builtin: boolean;
|
|
56
|
+
hidden: boolean;
|
|
57
|
+
};
|
|
58
|
+
/** The event a defined name runs a macro on, when Excel runs it on one: any name that starts with Auto_Open and so on. */
|
|
59
|
+
export declare function autoName(name: string): {
|
|
60
|
+
event: string;
|
|
61
|
+
onOpen: boolean;
|
|
62
|
+
} | undefined;
|