@patrickdk77/office-defuse 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +89 -11
- package/dist/biff.d.ts +11 -0
- package/dist/biff.js +15 -0
- package/dist/contained.d.ts +15 -5
- package/dist/contained.js +11 -7
- package/dist/context.d.ts +20 -6
- package/dist/context.js +28 -3
- package/dist/engine.js +7 -6
- package/dist/fields.js +1 -1
- package/dist/findings.js +9 -3
- package/dist/flat.js +4 -7
- package/dist/forms.d.ts +51 -0
- package/dist/forms.js +316 -0
- package/dist/macros.d.ts +26 -0
- package/dist/macros.js +160 -0
- package/dist/msg.js +1 -1
- package/dist/objects.js +76 -17
- package/dist/odf.js +9 -10
- package/dist/ole.d.ts +3 -3
- package/dist/ole.js +55 -5
- package/dist/ooxml.d.ts +26 -0
- package/dist/ooxml.js +163 -15
- package/dist/ppt-writer.d.ts +27 -0
- package/dist/ppt-writer.js +105 -0
- package/dist/ppt.d.ts +5 -1
- package/dist/ppt.js +179 -46
- package/dist/rtf.js +4 -2
- package/dist/score.d.ts +2 -0
- package/dist/score.js +12 -0
- package/dist/types.d.ts +44 -0
- package/dist/types.js +3 -0
- package/dist/vba.d.ts +78 -0
- package/dist/vba.js +378 -0
- package/dist/xls-writer.d.ts +34 -0
- package/dist/xls-writer.js +250 -0
- package/dist/xls.d.ts +9 -1
- package/dist/xls.js +126 -43
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -77,7 +77,7 @@ The format comes from the content, never from the file name.
|
|
|
77
77
|
|---|---|---|---|
|
|
78
78
|
| Word | .docx .docm .dotx .dotm | .doc .dot from Word 6.0 to 2003, and .rtf | Flat OPC .xml |
|
|
79
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 |
|
|
80
|
+
| PowerPoint | .pptx .pptm .potx .potm .ppsx .ppsm .ppam .sldx .sldm | .ppt from PowerPoint 97 to 2003, and .ppa add-ins | Flat OPC .xml |
|
|
81
81
|
| Visio | .vsdx .vsdm .vstx .vstm .vssx .vssm | .vsd from Visio 2000 to 2010 | |
|
|
82
82
|
| XPS | .xps .oxps | | |
|
|
83
83
|
| OpenDocument | .odt .ott .ods .ots .odp .otp .odg .otg .odf .odm .odb | | .fodt .fods .fodp .fodg |
|
|
@@ -91,7 +91,7 @@ A ZIP with both markers is rejected as PACKAGE/AMBIGUOUS_FORMAT, since Office co
|
|
|
91
91
|
|
|
92
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.
|
|
93
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. Neither needs the password of an encrypted file. 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.
|
|
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. Neither needs the password of an encrypted file. An Outlook template has a class id of its own, and so does a PowerPoint 97 to 2003 add-in, a .ppa, whose root is its VBA project. A PowerPoint 97 to 2003 template or show reports its format as .ppt, and a Visio stencil or template as .vsd.
|
|
95
95
|
|
|
96
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
97
|
|
|
@@ -118,7 +118,7 @@ Detection does not claim a file whose format office-defuse cannot name. That cov
|
|
|
118
118
|
|
|
119
119
|
In a ZIP package:
|
|
120
120
|
|
|
121
|
-
- 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
|
|
121
|
+
- 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`, unless `scriptPlugins` is set. Then it reads the VBA project, as [Script plugins](#script-plugins) says.
|
|
122
122
|
- 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.
|
|
123
123
|
- 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.
|
|
124
124
|
- 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.
|
|
@@ -131,7 +131,7 @@ In a ZIP package:
|
|
|
131
131
|
- 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:`.
|
|
132
132
|
- 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.
|
|
133
133
|
- 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.
|
|
134
|
-
- Embedded OLE objects and packages, OLE files nothing declares as an object, Java applets, plug-ins and ActiveX controls.
|
|
134
|
+
- Embedded OLE objects and packages, OLE files nothing declares as an object, Java applets, plug-ins and ActiveX controls. A Forms 2.0 control whose persisted properties read as Office writes them is CONTROL/FORMS_CONTROL, `info`, and stays. See [ActiveX controls](#activex-controls).
|
|
135
135
|
- 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.
|
|
136
136
|
- Document type declarations in XML parts, and references to external entities. See [Document types and entities](#document-types-and-entities).
|
|
137
137
|
|
|
@@ -200,7 +200,7 @@ The check reads the output with the caller's overrides and limits, without `file
|
|
|
200
200
|
- 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.
|
|
201
201
|
- 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.
|
|
202
202
|
- 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.
|
|
203
|
-
- 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.
|
|
203
|
+
- 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. When script plugins keep the document's VBA project, a shape, form control, callback, action or MACROBUTTON field that runs one of its macros by name stays.
|
|
204
204
|
- The main part keeps its content type, so a `.docm` without its VBA project is still a `.docm`.
|
|
205
205
|
- 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.
|
|
206
206
|
- 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.
|
|
@@ -216,13 +216,50 @@ A Flat OPC package is rewritten as the package of its parts is, then written bac
|
|
|
216
216
|
|
|
217
217
|
### Compound files
|
|
218
218
|
|
|
219
|
-
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.
|
|
219
|
+
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, Word's picture for printing a control, and Outlook attachments. A VBA project that script plugins keep is written without its compiled code. A message's property stream counts its attachments, and the count drops by the ones that went.
|
|
220
220
|
|
|
221
221
|
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.
|
|
222
222
|
|
|
223
223
|
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.
|
|
224
224
|
|
|
225
|
-
Content inside
|
|
225
|
+
Excel's and PowerPoint's records are written again, as the next two sections say. Content inside any other stream stays as it was: Word's fields, template and WordBasic macros, Excel's cell formulas, and the ShapeSheet cells of a Visio drawing. A finding there is still in the output, so the output fails its check and the upload is rejected.
|
|
226
|
+
|
|
227
|
+
### Excel workbooks
|
|
228
|
+
|
|
229
|
+
A workbook stream, Excel 97 to 2003 or Excel 5.0 and 95, is written again record by record, in the order the stream holds them:
|
|
230
|
+
|
|
231
|
+
- An Excel 4.0 macro sheet becomes an empty worksheet of the same name and visibility, so formulas and names keep their sheet numbers.
|
|
232
|
+
- A name that runs a macro, such as Auto_Open, is renamed `Removed_Auto_Open_1` and so on, and is no longer a built-in name. Formulas refer to names by number, so removing it would point them at the wrong name.
|
|
233
|
+
- A button or shape loses the macro it runs, unless the workbook's VBA project stays and the macro is one of its own: one name of the SupBook that is the workbook itself, as Excel names a VBA macro. A defined name can stand for anything, so a button that runs one loses it.
|
|
234
|
+
- A hyperlink goes with its tooltip.
|
|
235
|
+
- An external workbook, DDE or OLE link keeps its SupBook record, which formulas count to by number, with a path that is the workbook itself, U+0002.
|
|
236
|
+
- A workbook encrypted with VelvetSweatshop is written decrypted, without its FILEPASS record. So is any encrypted workbook that is written again, since office-defuse does not encrypt.
|
|
237
|
+
|
|
238
|
+
Records change size, so the stream positions other records hold are written anew: each sheet's in BoundSheet8, the DefColWidth and DBCell positions in Index, a DBCell's offset back to its first row, and ExtSST's string positions. A position that lands where no record starts, or inside a record that changed, leaves the stream as it was, and the output check rejects the upload. Excel 5.0 and 95 workbooks get the sheet and name edits only. A cell formula that calls EXEC, CALL or WEBSERVICE is not written again.
|
|
239
|
+
|
|
240
|
+
### PowerPoint presentations
|
|
241
|
+
|
|
242
|
+
A presentation is written as a full save: each persist object the current edit reaches, in persist id order, then one persist directory and one UserEditAtom with no edit before it, which the Current User stream points at. A container's length follows the records left in it.
|
|
243
|
+
|
|
244
|
+
- An embedded object, a linked object or an ActiveX control that goes takes its ExOleObjStg and its container in ExObjList with it. A Forms 2.0 control stays. An object a file plugin rewrote takes the plugin's bytes, compressed again when PowerPoint stored it compressed.
|
|
245
|
+
- A VBA project that goes takes its VBAInfoContainer with it. One that script plugins keep is written without its compiled code inside its compressed storage.
|
|
246
|
+
- An action that runs a program, a macro or an OLE verb becomes no action. A macro of a kept VBA project keeps its action.
|
|
247
|
+
- A hyperlink that goes takes its ExHyperlinkContainer, and the actions that open it become no action.
|
|
248
|
+
- A linked movie or sound loses its path.
|
|
249
|
+
- An encrypted presentation that is written again is written decrypted, without its encryption session. Its Pictures and EncryptedSummary streams are encrypted too, and office-defuse does not decrypt them, so a presentation that has either stays as it was and the output check rejects it.
|
|
250
|
+
|
|
251
|
+
## ActiveX controls
|
|
252
|
+
|
|
253
|
+
An ActiveX control runs whatever code its class names, so a control goes unless office-defuse can tell it is one of the Forms 2.0 controls Office ships, as Office persisted it: CommandButton, Label, TextBox, ListBox, ComboBox, CheckBox, OptionButton, ToggleButton, SpinButton or ScrollBar. Such a control is CONTROL/FORMS_CONTROL, `info`, and stays, with or without a VBA project. An override to `strip` removes it like any other control.
|
|
254
|
+
|
|
255
|
+
office-defuse reads the control's properties as MS-OFORMS lays them out and as LibreOffice reads them, and keeps the control when:
|
|
256
|
+
|
|
257
|
+
- its class id, or the ProgID its CompObj, ax:ocx element or Obj record names, is one of those controls;
|
|
258
|
+
- every bit of its property mask names a property MS-OFORMS defines;
|
|
259
|
+
- it holds no picture or mouse icon, whose bytes are a picture file Office decodes;
|
|
260
|
+
- its properties, its font and, for a list, its column infos fill its data to the last byte.
|
|
261
|
+
|
|
262
|
+
The control can sit in an OOXML activeX part, persisted as a stream that starts with the class id, as a storage with a contents stream, or as a property bag with plain values. In a Word document it is a storage under ObjectPool, and its `\x03PRINT` stream, a Windows metafile Word prints in its place, goes as CONTROL/CONTROL_PICTURE. In an Excel workbook every control is in one `Ctls` stream, which stays only when the Obj records place Forms 2.0 controls over all of it. Any other control, a Forms 2.0 control with a picture, and a frame, multipage or tab strip are CONTROL/ACTIVEX with `data.reason`.
|
|
226
263
|
|
|
227
264
|
### RTF
|
|
228
265
|
|
|
@@ -248,9 +285,11 @@ A file comes as a `ContainedFile`. `name` is the part or entry name, a Packager
|
|
|
248
285
|
| `removed` | goes, with every reference to it | EMBEDDED/PLUGIN_REMOVED, `strip` |
|
|
249
286
|
| `scrubbed` with nothing written, or an error | goes, with every reference to it | EMBEDDED/PLUGIN_FAILED, `strip` |
|
|
250
287
|
|
|
251
|
-
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.
|
|
288
|
+
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. A file whose plugin failed gets that rule too, and goes.
|
|
252
289
|
|
|
253
|
-
|
|
290
|
+
A plugin's answer is final. A file a plugin passed, scrubbed or removed gets none of office-defuse's own findings: no EMBEDDED/PACKAGE, EMBEDDED/OLE_OBJECT or EMBEDDED/EQUATION, and no MACRO/VBA_PROJECT for `vbaProject.bin`. office-defuse does not look inside it either, so an object's storage is not searched for VBA projects or other objects. An Equation Editor object a plugin passes stays. Through file-defuse no plugin takes a bare object storage, so EMBEDDED/EQUATION still removes one there. 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.
|
|
291
|
+
|
|
292
|
+
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`, and they are all the file adds to the score. PLUGIN_PASSED and PLUGIN_SCRUBBED weigh 0, in the upload's score and the output's. 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.
|
|
254
293
|
|
|
255
294
|
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.
|
|
256
295
|
|
|
@@ -262,6 +301,42 @@ const { disarmOffice, officePlugin } = require('@patrickdk77/office-defuse');
|
|
|
262
301
|
await disarmOffice(bytes, { filePlugins: [officePlugin()] });
|
|
263
302
|
```
|
|
264
303
|
|
|
304
|
+
## Script plugins
|
|
305
|
+
|
|
306
|
+
`scriptPlugins` takes script plugins that decide about VBA. office-defuse reads a VBA project's dir stream and modules as MS-OVBA describes them, and shows each module to the plugins as a `ContainedScript`:
|
|
307
|
+
|
|
308
|
+
| Field | What it holds |
|
|
309
|
+
|---|---|
|
|
310
|
+
| `text` | The module's source, decoded with the project's code page |
|
|
311
|
+
| `trigger` | `vba-document`, `vba-standard`, `vba-class` or `vba-form`, as the PROJECT stream lists the module |
|
|
312
|
+
| `location` | Where the module sits, such as `xl/vbaProject.bin > VBA/Module1` |
|
|
313
|
+
| `fieldName` | The module's name |
|
|
314
|
+
| `language`, `form` | `vba` and `office` |
|
|
315
|
+
| `references` | On the first module only, the project's references, each `{ name, kind, libid }` |
|
|
316
|
+
|
|
317
|
+
The first plugin whose `accepts` returns true decides a module. A module it passes gets MACRO/PLUGIN_PASSED, `info`. Any other answer, `scrubbed` included, is MACRO/PLUGIN_REMOVED, and an error is MACRO/PLUGIN_FAILED, both `strip`, since office-defuse writes no module a plugin rewrote. The plugin's findings go into the report as they are.
|
|
318
|
+
|
|
319
|
+
The project stays when every module passed. Otherwise it goes, as it does without plugins, and so does a project with a module no plugin took. A project that stays is written without its compiled code, the way Mandiant's OfficePurge writes one. Each module stream holds only its source, compressed again, the dir stream says each source starts at 0, `_VBA_PROJECT` names no VBA version, and the `__SRP_` caches go. Office then compiles the source the plugins read, so p-code that does something other than its source, which VBA stomping relies on, never runs. MACRO/COMPILED_CODE, `strip`, reports the purge.
|
|
320
|
+
|
|
321
|
+
When a plugin passed or removed every module, the answers are final. The report holds MACRO/PLUGIN_PASSED and PLUGIN_REMOVED with the plugins' findings, and no MACRO/VBA_PROJECT, and the project scores what the plugins found in it. MACRO/VBA_PROJECT, at office-defuse's weight, reports a project with a module no plugin took or a plugin failed on, or one office-defuse removes for a reason of its own, such as a user form that holds an ActiveX control.
|
|
322
|
+
|
|
323
|
+
A user form stays with its project when its designer storage holds Forms 2.0 controls only. The designer must be the UserForm designer, the storages under it must be Forms 2.0 forms, frames and multipages, and every form's class table must be empty, since a class table can name any ActiveX control. A form that fails gets CONTROL/ACTIVEX at its designer, and while that finding strips, the project goes.
|
|
324
|
+
|
|
325
|
+
When the document's own project stays, so does what runs its macros by name: an Excel shape's `macro`, a form control's `x:FmlaMacro`, a ribbon callback, PowerPoint's `ppaction://macro` and Word's MACROBUTTON field. Their findings turn to `info`. A name that points into another file, as `'other.xlsm'!Macro`, still goes. Once the plugins answered for the project, kept or removed, these findings weigh 0, since the plugins scored the macros they run.
|
|
326
|
+
|
|
327
|
+
office-defuse decides the `vbaProject.bin` part of an OOXML package, which with `scriptPlugins` set, even to an empty list, goes to the script plugins and not to the file plugins. It decides the project storages of a Word document, a workbook, a PowerPoint 97 presentation, a .ppa add-in and the other compound files. A project inside an embedded object is for the file plugins that take the object.
|
|
328
|
+
|
|
329
|
+
A source that does not decompress, or a dir stream that names a module stream that is not there, leaves nothing to check. The project goes, and MACRO/VBA_PROJECT's `data.reason` says why. The check of the output does not ask the plugins again. It matches the project by the MD5 of its references and module sources, reports the plugins' findings again, and reports MACRO/COMPILED_CODE if the output still holds p-code, which fails the check.
|
|
330
|
+
|
|
331
|
+
[script-defuse](https://github.com/patrickdk77/script-defuse)'s `formScriptPlugin` is a script plugin for VBA:
|
|
332
|
+
|
|
333
|
+
```js
|
|
334
|
+
const { disarmOffice } = require('@patrickdk77/office-defuse');
|
|
335
|
+
const { formScriptPlugin } = require('@patrickdk77/script-defuse');
|
|
336
|
+
|
|
337
|
+
await disarmOffice(bytes, { scriptPlugins: [formScriptPlugin()] });
|
|
338
|
+
```
|
|
339
|
+
|
|
265
340
|
## The containers
|
|
266
341
|
|
|
267
342
|
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:
|
|
@@ -382,6 +457,8 @@ Three combinations add more. Code here means macros or DDE. Code or a run-progra
|
|
|
382
457
|
|
|
383
458
|
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.
|
|
384
459
|
|
|
460
|
+
Content a plugin answered for scores at the weights of the plugin's findings alone, as [File plugins](#file-plugins) and [Script plugins](#script-plugins) describe. A workbook whose VBA project script-defuse keeps scores only what is left, such as the 10 of MACRO/COMPILED_CODE.
|
|
461
|
+
|
|
385
462
|
## Options
|
|
386
463
|
|
|
387
464
|
| Option | Default | Purpose |
|
|
@@ -402,6 +479,7 @@ The bands are NONE at 0, LOW from 1, MEDIUM from 25, HIGH from 50 and CRITICAL f
|
|
|
402
479
|
| `actionOverrides` | none | Change the action for a category, or for a category and detail |
|
|
403
480
|
| `scoreWeights`, `scoreBands` | built in | Change the scoring |
|
|
404
481
|
| `filePlugins` | none | Plugins that decide about the files inside the file. See [File plugins](#file-plugins) |
|
|
482
|
+
| `scriptPlugins` | none | Plugins that decide about the modules of a VBA project. See [Script plugins](#script-plugins) |
|
|
405
483
|
| `tempDir` | `os.tmpdir()` | Where temporary files go |
|
|
406
484
|
| `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 |
|
|
407
485
|
|
|
@@ -447,8 +525,8 @@ Exit codes are 0 for clean, 1 for defused or strippable, 2 for rejected and 3 fo
|
|
|
447
525
|
|
|
448
526
|
## What it does not check
|
|
449
527
|
|
|
450
|
-
- The OLE parts of a ZIP package, such as
|
|
451
|
-
- VBA source.
|
|
528
|
+
- The OLE parts of a ZIP package, such as the objects inside `oleObject*.bin`, the binaries of ActiveX controls other than Forms 2.0 ones, and `vbaProject.bin` without script plugins. office-defuse hands them to the file plugins and reads none of them itself.
|
|
529
|
+
- VBA source. office-defuse reads the modules only to show them to script plugins, and without plugins reports a project whole. In a user form that stays, the controls' data and pictures in its `o` stream go unread.
|
|
452
530
|
- 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.
|
|
453
531
|
- 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.
|
|
454
532
|
- 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.
|
package/dist/biff.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ export interface BiffRecord {
|
|
|
24
24
|
/** A sheet as BoundSheet8 (or BIFF5's BOUNDSHEET) lists it. */
|
|
25
25
|
export interface Sheet {
|
|
26
26
|
position: number;
|
|
27
|
+
/** Where its BoundSheet record starts in the stream. */
|
|
28
|
+
record?: number;
|
|
27
29
|
/** 0 visible, 1 hidden, 2 very hidden. */
|
|
28
30
|
state: number;
|
|
29
31
|
/** 0 worksheet or dialog, 1 macro sheet, 2 chart, 6 VBA module. */
|
|
@@ -42,6 +44,15 @@ export declare class RecordReader {
|
|
|
42
44
|
constructor(cfb: CompoundFile, stream: CfbEntry, start: number, deadline?: number | undefined);
|
|
43
45
|
next(): Promise<BiffRecord | undefined>;
|
|
44
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* The subrecords of a BIFF8 Obj record (MS-XLS 2.4.181), each with its type, where its header starts and its data. The
|
|
49
|
+
* walk stops at FtEnd, and at FtLbsData, whose length does not count all its data.
|
|
50
|
+
*/
|
|
51
|
+
export declare function objSubrecords(b: Uint8Array): Generator<{
|
|
52
|
+
ft: number;
|
|
53
|
+
at: number;
|
|
54
|
+
data: Uint8Array;
|
|
55
|
+
}>;
|
|
45
56
|
/** BIFF8 writes strings with a flag byte: 8-bit characters, or 16-bit ones when its low bit is set. */
|
|
46
57
|
export declare function xlString(b: Uint8Array, at: number, chars: number): {
|
|
47
58
|
text: string;
|
package/dist/biff.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.RecordReader = exports.R = void 0;
|
|
4
|
+
exports.objSubrecords = objSubrecords;
|
|
4
5
|
exports.xlString = xlString;
|
|
5
6
|
exports.parseBoundSheet = parseBoundSheet;
|
|
6
7
|
exports.parseLbl = parseLbl;
|
|
@@ -56,6 +57,20 @@ class RecordReader {
|
|
|
56
57
|
}
|
|
57
58
|
}
|
|
58
59
|
exports.RecordReader = RecordReader;
|
|
60
|
+
/**
|
|
61
|
+
* The subrecords of a BIFF8 Obj record (MS-XLS 2.4.181), each with its type, where its header starts and its data. The
|
|
62
|
+
* walk stops at FtEnd, and at FtLbsData, whose length does not count all its data.
|
|
63
|
+
*/
|
|
64
|
+
function* objSubrecords(b) {
|
|
65
|
+
for (let at = 0; at + 4 <= b.length;) {
|
|
66
|
+
const ft = b[at] | (b[at + 1] << 8);
|
|
67
|
+
const cb = b[at + 2] | (b[at + 3] << 8);
|
|
68
|
+
if (ft === 0x00 || ft === 0x13)
|
|
69
|
+
return;
|
|
70
|
+
yield { ft, at, data: b.subarray(at + 4, at + 4 + cb) };
|
|
71
|
+
at += 4 + cb;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
59
74
|
/** BIFF8 writes strings with a flag byte: 8-bit characters, or 16-bit ones when its low bit is set. */
|
|
60
75
|
function xlString(b, at, chars) {
|
|
61
76
|
if (at >= b.length)
|
package/dist/contained.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { RunContext, RunState } from './context';
|
|
1
|
+
import type { Reported, RunContext, RunState } from './context';
|
|
2
2
|
import { SpillSink } from './io';
|
|
3
|
-
import { type DefuseFinding, type OfficeOptions, type PluginContext } from './types';
|
|
3
|
+
import { OfficeCategory as C, OfficeDetail as D, type DefuseFinding, type OfficeOptions, type PluginContext } from './types';
|
|
4
4
|
/** The key under which office-defuse hands its run's options to a plugin. No other package can name it. */
|
|
5
5
|
export declare const RUN_OPTIONS: unique symbol;
|
|
6
6
|
/** A plugin context as office-defuse makes it. */
|
|
@@ -10,6 +10,11 @@ export type RunPluginContext = PluginContext & {
|
|
|
10
10
|
/** What the plugins decided about a file inside the file. Undefined from `decideContained` means no plugin took it. */
|
|
11
11
|
export interface ContainedDecision {
|
|
12
12
|
keep: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* A plugin passed, cleaned or removed the file. Its answer is final: the report holds it and what the plugin found,
|
|
15
|
+
* and office-defuse reports nothing of its own about the file.
|
|
16
|
+
*/
|
|
17
|
+
answered?: boolean;
|
|
13
18
|
/** The bytes a plugin kept in place of the file's own. */
|
|
14
19
|
replacement?: SpillSink;
|
|
15
20
|
}
|
|
@@ -22,6 +27,11 @@ export interface InnerFile {
|
|
|
22
27
|
/** The file's bytes, in order. */
|
|
23
28
|
chunks(): AsyncIterable<Uint8Array>;
|
|
24
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Throws on an error a plugin threw that stops the whole tree. An I/O error fails this run as one of its own would,
|
|
32
|
+
* so its cause goes up.
|
|
33
|
+
*/
|
|
34
|
+
export declare function passOn(run: RunState, e: unknown): void;
|
|
25
35
|
/**
|
|
26
36
|
* Adds the findings a plugin reported from inside a file, labeled with the file's name and place. A reject inside the
|
|
27
37
|
* file means the file goes, not the one that holds it, so it is recorded here as strip.
|
|
@@ -38,9 +48,9 @@ export declare function pushContained(ctx: RunContext, found: DefuseFinding[] |
|
|
|
38
48
|
*/
|
|
39
49
|
export declare function decideContained(ctx: RunContext, inner: InnerFile): Promise<ContainedDecision | undefined>;
|
|
40
50
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
51
|
+
* Reports the rule a file gets for being there, such as EMBEDDED/OLE_OBJECT, when no plugin answered for the file. A
|
|
52
|
+
* plugin's answer is final: when one answered, nothing is reported, and the result only says whether the file stays.
|
|
43
53
|
*/
|
|
44
|
-
export declare function
|
|
54
|
+
export declare function ownFinding(ctx: RunContext, decision: ContainedDecision | undefined, category: C, detail: D, location: string, data?: Record<string, string | number>): Reported;
|
|
45
55
|
/** Bytes in a spill sink the run holds until it ends, as the rewrite takes a plugin's output. */
|
|
46
56
|
export declare function spillOf(run: RunState, bytes: Uint8Array): Promise<SpillSink>;
|
package/dist/contained.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.RUN_OPTIONS = void 0;
|
|
4
|
+
exports.passOn = passOn;
|
|
4
5
|
exports.pushContained = pushContained;
|
|
5
6
|
exports.decideContained = decideContained;
|
|
6
|
-
exports.
|
|
7
|
+
exports.ownFinding = ownFinding;
|
|
7
8
|
exports.spillOf = spillOf;
|
|
8
9
|
const errors_1 = require("./errors");
|
|
9
10
|
const io_1 = require("./io");
|
|
@@ -64,7 +65,7 @@ async function decideContained(ctx, inner) {
|
|
|
64
65
|
if (expected) {
|
|
65
66
|
ctx.report(types_1.OfficeCategory.Embedded, expected.result === 'passed' ? types_1.OfficeDetail.PluginPassed : types_1.OfficeDetail.PluginScrubbed, inner.location, { plugin: expected.plugin, name: name ?? '' });
|
|
66
67
|
pushContained(ctx, expected.findings, inner.location, name);
|
|
67
|
-
return { keep: true };
|
|
68
|
+
return { keep: true, answered: true };
|
|
68
69
|
}
|
|
69
70
|
if (!plugins.length)
|
|
70
71
|
return undefined;
|
|
@@ -107,6 +108,7 @@ async function decideContained(ctx, inner) {
|
|
|
107
108
|
}
|
|
108
109
|
else
|
|
109
110
|
ctx.removal(types_1.OfficeCategory.Embedded, r.result === 'removed' ? types_1.OfficeDetail.PluginRemoved : types_1.OfficeDetail.PluginFailed, inner.location, data);
|
|
111
|
+
const answered = keep || r.result === 'removed';
|
|
110
112
|
pushContained(ctx, r.findings, inner.location, name);
|
|
111
113
|
const replacement = keep && out.length > 0 ? out : undefined;
|
|
112
114
|
if (replacement) {
|
|
@@ -116,7 +118,7 @@ async function decideContained(ctx, inner) {
|
|
|
116
118
|
}
|
|
117
119
|
else
|
|
118
120
|
await out.dispose();
|
|
119
|
-
return { keep, replacement };
|
|
121
|
+
return { keep, replacement, answered };
|
|
120
122
|
}
|
|
121
123
|
catch (e) {
|
|
122
124
|
await out.dispose();
|
|
@@ -136,11 +138,13 @@ async function decideContained(ctx, inner) {
|
|
|
136
138
|
}
|
|
137
139
|
}
|
|
138
140
|
/**
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
+
* Reports the rule a file gets for being there, such as EMBEDDED/OLE_OBJECT, when no plugin answered for the file. A
|
|
142
|
+
* plugin's answer is final: when one answered, nothing is reported, and the result only says whether the file stays.
|
|
141
143
|
*/
|
|
142
|
-
function
|
|
143
|
-
|
|
144
|
+
function ownFinding(ctx, decision, category, detail, location, data) {
|
|
145
|
+
if (decision?.answered)
|
|
146
|
+
return { action: decision.keep ? 'info' : 'strip' };
|
|
147
|
+
return ctx.report(category, detail, location, data);
|
|
144
148
|
}
|
|
145
149
|
/** Bytes in a spill sink the run holds until it ends, as the rewrite takes a plugin's output. */
|
|
146
150
|
async function spillOf(run, bytes) {
|
package/dist/context.d.ts
CHANGED
|
@@ -68,15 +68,17 @@ export interface RunState {
|
|
|
68
68
|
kept: Map<string, Expected>;
|
|
69
69
|
/** Plugin output that waits for the rewrite. The run disposes of it when it ends. */
|
|
70
70
|
held: SpillSink[];
|
|
71
|
-
/**
|
|
72
|
-
* Findings whose content a plugin kept, such as EMBEDDED/PACKAGE for a package a plugin passed. They stay in the
|
|
73
|
-
* report as info and leave the score to what the plugin found, as pdf-defuse leaves out a kept attached file.
|
|
74
|
-
*/
|
|
75
|
-
superseded: WeakSet<DefuseFinding>;
|
|
76
71
|
/** Sources the rewrite reads from, such as a compound file a plugin returned, which the run closes when it ends. */
|
|
77
72
|
sources: ByteSource[];
|
|
78
73
|
/** The container, once the format rules have read it. */
|
|
79
74
|
output?: Output;
|
|
75
|
+
/**
|
|
76
|
+
* The document's own VBA project stays, as the script plugins passed it. The buttons, shapes, callbacks and fields
|
|
77
|
+
* that run one of its macros by name then stay too.
|
|
78
|
+
*/
|
|
79
|
+
macrosKept?: boolean;
|
|
80
|
+
/** The script plugins answered for the document's own VBA project, whether they kept it or removed it. */
|
|
81
|
+
macrosAnswered?: boolean;
|
|
80
82
|
}
|
|
81
83
|
/** A finding the rules made, with its action, which the rewrite reads. Past 200 of one kind, `finding` is absent. */
|
|
82
84
|
export interface Reported {
|
|
@@ -115,11 +117,23 @@ export declare class RunContext {
|
|
|
115
117
|
withPlan(): PlanContext;
|
|
116
118
|
/**
|
|
117
119
|
* Records a finding. Past 200 of one kind, as in pdf-defuse, one more finding counts the rest. `action` replaces the
|
|
118
|
-
* action the overrides give, for content whose fate a plugin decided.
|
|
120
|
+
* action the overrides give, for content whose fate a plugin decided. Such a finding as info weighs nothing.
|
|
119
121
|
*/
|
|
120
122
|
report(category: C, detail: D, location?: string, data?: Record<string, string | number>, action?: OfficeFindingAction): Reported;
|
|
123
|
+
/**
|
|
124
|
+
* Reports a link, callback or field that runs the macro `name`. When the script plugins answered for the document's
|
|
125
|
+
* own VBA project, a trigger of one of its macros follows their answer: it stays, as info, when they kept the
|
|
126
|
+
* project, and the overrides decide it when they removed the project. Either way it weighs nothing, since the
|
|
127
|
+
* plugins scored the macro it runs.
|
|
128
|
+
*/
|
|
129
|
+
macroTrigger(category: C, detail: D, location: string, data: Record<string, string | number>, name: string | undefined): Reported;
|
|
121
130
|
/** Reports content that goes whatever the overrides say: its action is strip, or reject when the caller asked for that. */
|
|
122
131
|
removal(category: C, detail: D, location?: string, data?: Record<string, string | number>): Reported;
|
|
132
|
+
/**
|
|
133
|
+
* Takes back a finding reported before a plugin answered for its content, whose answer is final. A finding past the
|
|
134
|
+
* first 200 of its kind was only counted, so there is none to take back.
|
|
135
|
+
*/
|
|
136
|
+
retract(finding: DefuseFinding | undefined): void;
|
|
123
137
|
/** Adds findings from another package as they are, such as those a plugin made inside a file. */
|
|
124
138
|
push(findings: DefuseFinding[]): void;
|
|
125
139
|
/** Reports a link, classified by pdf-defuse's checks of its target. */
|
package/dist/context.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.ZipContext = exports.PlanContext = exports.RunContext = exports.clip = void 0;
|
|
4
4
|
const links_1 = require("./links");
|
|
5
5
|
const plan_1 = require("./plan");
|
|
6
|
+
const score_1 = require("./score");
|
|
6
7
|
const types_1 = require("./types");
|
|
7
8
|
const xml_1 = require("./xml");
|
|
8
9
|
const OLE_SIGNATURE = [0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1];
|
|
@@ -12,6 +13,8 @@ const clip = (raw, n = 200) => {
|
|
|
12
13
|
return s.length > n ? `${s.slice(0, n - 3)}...` : s;
|
|
13
14
|
};
|
|
14
15
|
exports.clip = clip;
|
|
16
|
+
/** A macro of the document's own project, by name: Macro, Module.Macro, or Excel's [0]!Macro for this workbook. */
|
|
17
|
+
const OWN_MACRO = /^(?:\[0\]!)?[\p{L}_][\p{L}\p{N}_]*(?:\.[\p{L}_][\p{L}\p{N}_]*)?$/u;
|
|
15
18
|
/** What one inspection holds while the format rules run. */
|
|
16
19
|
class RunContext {
|
|
17
20
|
factory;
|
|
@@ -51,7 +54,7 @@ class RunContext {
|
|
|
51
54
|
}
|
|
52
55
|
/**
|
|
53
56
|
* Records a finding. Past 200 of one kind, as in pdf-defuse, one more finding counts the rest. `action` replaces the
|
|
54
|
-
* action the overrides give, for content whose fate a plugin decided.
|
|
57
|
+
* action the overrides give, for content whose fate a plugin decided. Such a finding as info weighs nothing.
|
|
55
58
|
*/
|
|
56
59
|
report(category, detail, location, data, action) {
|
|
57
60
|
const act = action ?? this.factory.actionFor(category, detail);
|
|
@@ -64,7 +67,7 @@ class RunContext {
|
|
|
64
67
|
const finding = this.factory.make(category, detail, location, data);
|
|
65
68
|
finding.action = act;
|
|
66
69
|
if (action === 'info')
|
|
67
|
-
|
|
70
|
+
(0, score_1.pluginDecided)(finding);
|
|
68
71
|
this.shared.findings.push(finding);
|
|
69
72
|
return { action: act, finding };
|
|
70
73
|
}
|
|
@@ -76,17 +79,39 @@ class RunContext {
|
|
|
76
79
|
const finding = this.factory.make(category, detail, 'additional occurrences', counted);
|
|
77
80
|
finding.action = act;
|
|
78
81
|
if (action === 'info')
|
|
79
|
-
|
|
82
|
+
(0, score_1.pluginDecided)(finding);
|
|
80
83
|
this.shared.findings.push(finding);
|
|
81
84
|
}
|
|
82
85
|
counted.count = Number(counted.count) + 1;
|
|
83
86
|
return { action: act };
|
|
84
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Reports a link, callback or field that runs the macro `name`. When the script plugins answered for the document's
|
|
90
|
+
* own VBA project, a trigger of one of its macros follows their answer: it stays, as info, when they kept the
|
|
91
|
+
* project, and the overrides decide it when they removed the project. Either way it weighs nothing, since the
|
|
92
|
+
* plugins scored the macro it runs.
|
|
93
|
+
*/
|
|
94
|
+
macroTrigger(category, detail, location, data, name) {
|
|
95
|
+
const own = name !== undefined && this.run.macrosAnswered === true && OWN_MACRO.test(name.trim());
|
|
96
|
+
const r = this.report(category, detail, location, data, own && this.run.macrosKept ? 'info' : undefined);
|
|
97
|
+
if (own && r.finding)
|
|
98
|
+
(0, score_1.pluginDecided)(r.finding);
|
|
99
|
+
return r;
|
|
100
|
+
}
|
|
85
101
|
/** Reports content that goes whatever the overrides say: its action is strip, or reject when the caller asked for that. */
|
|
86
102
|
removal(category, detail, location, data) {
|
|
87
103
|
const act = this.factory.actionFor(category, detail);
|
|
88
104
|
return this.report(category, detail, location, data, act === 'info' ? 'strip' : act);
|
|
89
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Takes back a finding reported before a plugin answered for its content, whose answer is final. A finding past the
|
|
108
|
+
* first 200 of its kind was only counted, so there is none to take back.
|
|
109
|
+
*/
|
|
110
|
+
retract(finding) {
|
|
111
|
+
const i = finding ? this.shared.findings.indexOf(finding) : -1;
|
|
112
|
+
if (i >= 0)
|
|
113
|
+
this.shared.findings.splice(i, 1);
|
|
114
|
+
}
|
|
90
115
|
/** Adds findings from another package as they are, such as those a plugin made inside a file. */
|
|
91
116
|
push(findings) {
|
|
92
117
|
this.shared.findings.push(...findings);
|
package/dist/engine.js
CHANGED
|
@@ -59,21 +59,21 @@ const xmlLimitsOf = (limits) => ({
|
|
|
59
59
|
entityExpansions: limits.xmlEntityExpansions,
|
|
60
60
|
entitySize: limits.xmlEntitySize,
|
|
61
61
|
});
|
|
62
|
-
const PLUGIN_KEPT = new Set([types_1.OfficeDetail.PluginPassed, types_1.OfficeDetail.PluginScrubbed]);
|
|
63
62
|
/**
|
|
64
63
|
* `incomplete` marks a run that stopped before it read the whole package. It is rejected without a score, whatever
|
|
65
64
|
* the actions say, because an override can make the finding that stopped it look harmless. In the check of an output,
|
|
66
65
|
* `isOutput`, the files plugins kept count in the score; in an upload they do not, and a file a plugin cleaned makes
|
|
67
66
|
* the upload strippable, since the output must be written.
|
|
68
67
|
*/
|
|
69
|
-
function finish(findings, options, incomplete, format, isOutput = false
|
|
68
|
+
function finish(findings, options, incomplete, format, isOutput = false) {
|
|
70
69
|
const named = { format: format?.format ?? '', mimeType: format?.mimeType ?? '' };
|
|
71
70
|
if (incomplete)
|
|
72
71
|
return { status: 'rejected', score: null, risk: types_1.OfficeRisk.Unknown, findings, ...named };
|
|
73
72
|
const rejected = findings.some(f => f.action === 'reject');
|
|
74
73
|
const strippable = findings.some(f => f.action === 'strip' || (!isOutput && f.detail === types_1.OfficeDetail.PluginScrubbed));
|
|
75
|
-
|
|
76
|
-
|
|
74
|
+
// What a plugin decided counts at the weights of the plugin's own findings; the PLUGIN_* findings that record its
|
|
75
|
+
// answer weigh nothing.
|
|
76
|
+
const { score, risk } = (0, score_1.scoreFindings)(findings, options);
|
|
77
77
|
return { status: rejected ? 'rejected' : strippable ? 'strippable' : 'clean', score, risk, findings, ...named };
|
|
78
78
|
}
|
|
79
79
|
/**
|
|
@@ -167,7 +167,6 @@ function newRun(options, depth, deadline, verify) {
|
|
|
167
167
|
verify,
|
|
168
168
|
kept: new Map(),
|
|
169
169
|
held: [],
|
|
170
|
-
superseded: new WeakSet(),
|
|
171
170
|
sources: [],
|
|
172
171
|
};
|
|
173
172
|
}
|
|
@@ -246,7 +245,7 @@ async function analyze(run, source) {
|
|
|
246
245
|
findings.push(...ctx.findings);
|
|
247
246
|
if (run.temp.ioError !== undefined)
|
|
248
247
|
throw run.temp.ioError;
|
|
249
|
-
return finish(findings, options, false, ctx.format, isOutput
|
|
248
|
+
return finish(findings, options, false, ctx.format, isOutput);
|
|
250
249
|
}
|
|
251
250
|
catch (e) {
|
|
252
251
|
if (src.failure !== undefined)
|
|
@@ -401,6 +400,8 @@ async function disarmAtDepth(source, sink, options, depth, deadline = options.li
|
|
|
401
400
|
}
|
|
402
401
|
// The output is checked with the caller's overrides and limits, and the plugins' verdicts in place of the plugins.
|
|
403
402
|
const verifyOptions = {
|
|
403
|
+
// No plugin is asked again. An empty list still reads VBA project parts the way the upload's run did.
|
|
404
|
+
scriptPlugins: options.scriptPlugins && [],
|
|
404
405
|
actionOverrides: options.actionOverrides,
|
|
405
406
|
scoreWeights: options.scoreWeights,
|
|
406
407
|
scoreBands: options.scoreBands,
|
package/dist/fields.js
CHANGED
|
@@ -29,7 +29,7 @@ function evaluateField(ctx, location, code, computed, result = '') {
|
|
|
29
29
|
if (name === 'DDE' || name === 'DDEAUTO')
|
|
30
30
|
actions.push(ctx.report(types_1.OfficeCategory.Dde, types_1.OfficeDetail.Field, location, data).action);
|
|
31
31
|
else if (name === 'MACROBUTTON')
|
|
32
|
-
actions.push(ctx.
|
|
32
|
+
actions.push(ctx.macroTrigger(types_1.OfficeCategory.Field, types_1.OfficeDetail.Macrobutton, location, { ...data, macro: (0, context_1.clip)(words[1]?.text ?? '') }, words[1]?.text ?? '').action);
|
|
33
33
|
else if (name === 'INCLUDETEXT' || name === 'INCLUDEPICTURE' || name === 'IMPORT' || name === 'LINK') {
|
|
34
34
|
const target = arg(name === 'LINK' ? 2 : 1);
|
|
35
35
|
if (target === undefined)
|
package/dist/findings.js
CHANGED
|
@@ -75,6 +75,10 @@ def(types_1.OfficeCategory.Encrypted, types_1.OfficeDetail.XorObfuscation, 'info
|
|
|
75
75
|
def(types_1.OfficeCategory.Encrypted, types_1.OfficeDetail.UnsupportedAlgorithm, 'reject', 0, 'The file is encrypted with an algorithm office-defuse does not decrypt<: {algorithm}|>');
|
|
76
76
|
// MACRO
|
|
77
77
|
def(types_1.OfficeCategory.Macro, types_1.OfficeDetail.VbaProject, 'strip', 70, 'The document holds VBA macros');
|
|
78
|
+
def(types_1.OfficeCategory.Macro, types_1.OfficeDetail.CompiledCode, 'strip', 10, "The VBA project's compiled code was removed, so Office compiles the source the plugins checked");
|
|
79
|
+
def(types_1.OfficeCategory.Macro, types_1.OfficeDetail.PluginPassed, 'info', 0, 'A VBA module was kept by <plugin {plugin}|a plugin>');
|
|
80
|
+
def(types_1.OfficeCategory.Macro, types_1.OfficeDetail.PluginRemoved, 'strip', 0, '<Plugin {plugin}|A plugin> did not keep a VBA module, so the VBA project was removed');
|
|
81
|
+
def(types_1.OfficeCategory.Macro, types_1.OfficeDetail.PluginFailed, 'strip', 0, '<Plugin {plugin}|A plugin> failed on a VBA module, so the VBA project was removed');
|
|
78
82
|
def(types_1.OfficeCategory.Macro, types_1.OfficeDetail.XlmMacrosheet, 'strip', 70, 'The workbook holds an Excel 4.0 macro sheet');
|
|
79
83
|
def(types_1.OfficeCategory.Macro, types_1.OfficeDetail.WordBasic, 'strip', 70, 'The Word 6.0 or 95 document holds WordBasic macros, which Word runs as VBA');
|
|
80
84
|
def(types_1.OfficeCategory.Macro, types_1.OfficeDetail.OdfBasic, 'strip', 70, 'The document holds Basic macros');
|
|
@@ -133,12 +137,14 @@ def(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Applet, 'strip', 60, '
|
|
|
133
137
|
def(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Plugin, 'strip', 50, 'An embedded browser plug-in object');
|
|
134
138
|
def(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Equation, 'strip', 70, 'An Equation Editor 3.0 object, which CVE-2017-11882 and CVE-2018-0802 exploit');
|
|
135
139
|
def(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Attachment, 'strip', 30, '<An attached file "{name}"|An attached file> (<{type}|unknown type>)');
|
|
136
|
-
def(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.PluginPassed, 'info',
|
|
137
|
-
def(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.PluginScrubbed, 'info',
|
|
140
|
+
def(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.PluginPassed, 'info', 0, 'An embedded file was kept by <plugin {plugin}|a plugin> with nothing removed');
|
|
141
|
+
def(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.PluginScrubbed, 'info', 0, 'An embedded file was cleaned and kept by <plugin {plugin}|a plugin>');
|
|
138
142
|
def(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.PluginFailed, 'strip', 0, '<Plugin {plugin}|A plugin> failed, so the embedded file was removed');
|
|
139
143
|
def(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.PluginRemoved, 'strip', 0, 'An embedded file was removed by <plugin {plugin}|a plugin>');
|
|
140
144
|
// CONTROL
|
|
141
|
-
def(types_1.OfficeCategory.Control, types_1.OfficeDetail.ActiveX, 'strip', 50, 'An ActiveX control');
|
|
145
|
+
def(types_1.OfficeCategory.Control, types_1.OfficeDetail.ActiveX, 'strip', 50, 'An ActiveX control<: {reason}|>');
|
|
146
|
+
def(types_1.OfficeCategory.Control, types_1.OfficeDetail.FormsControl, 'info', 10, 'A Forms 2.0 control<, {className}|>, whose persisted properties office-defuse read');
|
|
147
|
+
def(types_1.OfficeCategory.Control, types_1.OfficeDetail.ControlPicture, 'strip', 10, "A control's picture for printing, a Windows metafile");
|
|
142
148
|
// PROCESSING
|
|
143
149
|
def(types_1.OfficeCategory.Processing, types_1.OfficeDetail.VerificationFailed, 'reject', 0, 'The defused output did not pass its own inspection, so nothing was written');
|
|
144
150
|
/** The built-in spec for a kind of finding, or undefined for a kind office-defuse does not make, such as one from another package. */
|
package/dist/flat.js
CHANGED
|
@@ -281,13 +281,10 @@ async function decideBinaries(ctx, odf) {
|
|
|
281
281
|
const object = plan.objects.find(o => o.index === parent.index);
|
|
282
282
|
if (object) {
|
|
283
283
|
decided.add(parent.index);
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
else if (decision || object.strips)
|
|
284
|
+
// A plugin's answer on the object's file is final, so the object's own finding is taken back.
|
|
285
|
+
if (decision?.answered)
|
|
286
|
+
ctx.retract(object.finding);
|
|
287
|
+
if (!decision?.keep && (decision || object.strips))
|
|
291
288
|
plan.element(exports.FLAT_ODF_PART, parent.index, { remove: true });
|
|
292
289
|
}
|
|
293
290
|
else if (decision && !decision.keep)
|