@patrickdk77/script-defuse 0.1.0 → 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 +53 -6
- package/dist/engine.d.ts +2 -2
- package/dist/engine.js +8 -4
- package/dist/findings.js +11 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -1
- package/dist/index.mjs +1 -1
- package/dist/plugins.js +6 -2
- package/dist/types.d.ts +28 -9
- package/dist/types.js +12 -2
- package/dist/vba.d.ts +11 -0
- package/dist/vba.js +728 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# script-defuse
|
|
2
2
|
|
|
3
|
-
Tell a
|
|
3
|
+
Tell a document's harmless scripts from the rest. script-defuse parses a PDF form's script and calls it a form script when every call and every property it uses formats, checks or calculates form fields. It reads AcroForm JavaScript, and the JavaScript and FormCalc of XFA forms. It also reads the VBA modules of Office documents, and passes a module that calls nothing on its list of the ways a macro reaches outside its document. Any other script is unsafe, and its findings name what it reaches and on which line: a web address, a form submit, mail, another document or file, the network, a program, code built from a string, or text it hides.
|
|
4
4
|
|
|
5
|
-
It plugs into [pdf-defuse](https://github.com/patrickdk77/pdf-defuse) as a script plugin
|
|
5
|
+
It plugs into [pdf-defuse](https://github.com/patrickdk77/pdf-defuse) and [office-defuse](https://github.com/patrickdk77/office-defuse) as a script plugin. A fillable form keeps its calculations, formats and checks, and a workbook keeps the macros that only work on the workbook, while every other script goes. It parses JavaScript with [acorn](https://github.com/acornjs/acorn), its one runtime dependency, and follows the shapes of the other defuse packages.
|
|
6
6
|
|
|
7
7
|
```
|
|
8
8
|
npm install @patrickdk77/script-defuse
|
|
@@ -35,9 +35,22 @@ const result = await disarmPdf(bytes, { scriptPlugins: [formScriptPlugin()] });
|
|
|
35
35
|
|
|
36
36
|
pdf-defuse removes every script unless a plugin keeps it. This plugin keeps the form scripts and removes the rest. From pdf-defuse 0.2.4 the plugin's findings go into the PDF's report at each script's location.
|
|
37
37
|
|
|
38
|
+
The plugin's answer is final, and its findings are what the script scores. Each finding's `weight` is what it adds to that script's score from `inspectScript`: a finding that strips weighs what `scoreWeights` or the table gives it, and one that only describes the script weighs 0. A form script the plugin keeps therefore adds nothing to the PDF's score from pdf-defuse 0.3.0 and office-defuse 0.3.0. Earlier versions scored a kept script at their own weight for it.
|
|
39
|
+
|
|
38
40
|
Make one plugin per PDF. pdf-defuse hands it the document-level scripts first, and the plugin remembers the globals of the ones it kept, so a field script may call a helper they define. A field script that calls a helper from a document script the plugin removed is removed too.
|
|
39
41
|
|
|
40
|
-
With [file-defuse](https://github.com/patrickdk77/file-defuse)
|
|
42
|
+
With [file-defuse](https://github.com/patrickdk77/file-defuse) 0.1.4 or later, every PDF and Office file already gets one.
|
|
43
|
+
|
|
44
|
+
### With office-defuse
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
const { disarmOffice } = require('@patrickdk77/office-defuse');
|
|
48
|
+
const { formScriptPlugin } = require('@patrickdk77/script-defuse');
|
|
49
|
+
|
|
50
|
+
const result = await disarmOffice(bytes, { scriptPlugins: [formScriptPlugin()] });
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
office-defuse 0.2.3 and later shows the plugin each module of a VBA project and keeps the project only when every module passes. It then writes the project without its compiled code, so Office compiles the source script-defuse read. See [VBA](#vba).
|
|
41
54
|
|
|
42
55
|
## What a form script may use
|
|
43
56
|
|
|
@@ -90,6 +103,26 @@ References such as `$host.messageBox`, `$.rawValue`, `xfa.event.change` and `$re
|
|
|
90
103
|
|
|
91
104
|
An XFA script defines no globals for other scripts, which reach its functions through the node that holds it.
|
|
92
105
|
|
|
106
|
+
## VBA
|
|
107
|
+
|
|
108
|
+
`context.language: 'vba'` reads a VBA module as Office stores it, with `form: 'office'`. A macro cannot have an allowlist the way a form script does. Its own procedures, the document's sheets and controls, and the whole Office object model are names no list holds. So script-defuse lists the ways out of the document instead, and removes a module that uses one:
|
|
109
|
+
|
|
110
|
+
- VBA's file statements and functions, such as `Open`, `Print #`, `Get`, `Put`, `Name`, `Kill`, `Dir` and `MkDir`. `Shell`, `SendKeys`, `AppActivate` and `MacScript`. `SaveSetting` and `GetSetting`, the DDE functions, and `Environ`.
|
|
111
|
+
- `Declare`, `VarPtr`, `StrPtr`, `ObjPtr` and `AddressOf`.
|
|
112
|
+
- `CreateObject`, `GetObject`, and `New` of a library's class. `New` of the module's own classes, `Collection`, `Dictionary` and `RegExp` is fine, and `New FileSystemObject` and `New Application` are not.
|
|
113
|
+
- Office members that open another file or a URL, send mail, save elsewhere, print, import or export, run code by name or later, or change the VBA project: `Workbooks.Open`, `FollowHyperlink`, `Hyperlinks.Add`, `AttachedTemplate`, `SendMail`, `SaveAs`, `SaveCopyAs`, `ExportAsFixedFormat`, `PrintOut`, `QueryTables`, `OLEObjects.Add`, `Application.Run` with an argument, `Evaluate` of anything but a string, `ExecuteExcel4Macro`, `OnTime`, `CallByName`, `WordBasic`, `VBProject`, `VBE`, `AddIns` and the others in `src/vba.ts`.
|
|
114
|
+
- A formula string that calls WEBSERVICE, CALL, REGISTER, EXEC, RTD or HYPERLINK, a DDE formula, and a macro name in another file, as `'other.xlsm'!Macro`.
|
|
115
|
+
|
|
116
|
+
A name VBA or Office defines counts wherever it is written, as `Shell`, `VBA.Shell` or `.Shell` inside a `With` block. A module that declares a variable of the same name does not hide it, since the name means VBA's again in the next procedure.
|
|
117
|
+
|
|
118
|
+
The project's references matter as much as its calls, since a library can add global functions a list never sees. office-defuse gives them with the project's first module, in `context.references`. script-defuse accepts VBA, OLE Automation, Office, Excel, Word, PowerPoint, Visio, Publisher, Project, Forms 2.0, the Ref Edit control, the Scripting runtime and VBScript regular expressions, by type library GUID. Any other library, and any project but Word's Normal template, is SCRIPT_EXTERNAL/REFERENCE.
|
|
119
|
+
|
|
120
|
+
VBA has its own ways to hide text, and each strips the module:
|
|
121
|
+
|
|
122
|
+
- `Chr` of a printable character six or more times in one statement, as `Chr(112) & Chr(111)`. A string literal could hold those characters, so only hiding them explains it. `Chr` of a control character, the quote or an accented letter is how macros write those, and stays.
|
|
123
|
+
- `Chr(Asc(Mid(s, i, 1)) Xor k)` and other decoders that rebuild a string a character at a time, `StrReverse`, and `Replace` of filler out of a literal.
|
|
124
|
+
- Strings that spell a payload word only once they are joined, as `"pow" & "ersh" & "ell"`, in one statement or appended to one variable across statements. The words are programs, script hosts, `http://`, file types that run, and the objects that fetch and write files. A joined string is checked for encoded data too. A long message joined from many literals hides nothing and stays.
|
|
125
|
+
|
|
93
126
|
## Obfuscation
|
|
94
127
|
|
|
95
128
|
A form script has nothing to hide, so each of these strips the script:
|
|
@@ -112,6 +145,7 @@ A form script has nothing to hide, so each of these strips the script:
|
|
|
112
145
|
| Kind | Action | Weight | When |
|
|
113
146
|
|---|---|---|---|
|
|
114
147
|
| SCRIPT_FORM/COMPUTATION | info | 0 | The script only formats, checks or calculates form fields |
|
|
148
|
+
| SCRIPT_FORM/DOCUMENT_MACRO | info | 0 | The VBA module calls nothing on the list of ways out of its document |
|
|
115
149
|
| SCRIPT_EXTERNAL/URL | strip | 60 | `app.launchURL`, `this.getURL`, `xfa.host.gotoURL`, setting a node's `href` |
|
|
116
150
|
| SCRIPT_EXTERNAL/SUBMIT | strip | 50 | `this.submitForm` |
|
|
117
151
|
| SCRIPT_EXTERNAL/MAIL | strip | 50 | `this.mailDoc`, `mailForm`, `app.mailMsg` |
|
|
@@ -122,6 +156,14 @@ A form script has nothing to hide, so each of these strips the script:
|
|
|
122
156
|
| SCRIPT_EXTERNAL/MENU_COMMAND | strip | 60 | `app.execMenuItem`, `app.execDialog` |
|
|
123
157
|
| SCRIPT_EXTERNAL/PRINT | strip | 20 | `this.print`, `xfa.host.print` |
|
|
124
158
|
| SCRIPT_EXTERNAL/SAVE | strip | 30 | `this.saveAs`, `closeDoc` |
|
|
159
|
+
| SCRIPT_EXTERNAL/FILE_SYSTEM | strip | 70 | VBA's file statements and functions, `FileSystemObject` |
|
|
160
|
+
| SCRIPT_EXTERNAL/PROCESS | strip | 90 | `Shell`, `SendKeys`, `AppActivate`, `MacScript`, `New Application`, a formula that calls EXEC |
|
|
161
|
+
| SCRIPT_EXTERNAL/REGISTRY | strip | 60 | `SaveSetting`, `GetSetting`, `PrivateProfileString` |
|
|
162
|
+
| SCRIPT_EXTERNAL/DDE | strip | 80 | `DDEInitiate` and the other DDE functions, a DDE formula |
|
|
163
|
+
| SCRIPT_EXTERNAL/COM_OBJECT | strip | 80 | `CreateObject`, `GetObject`, `New` of a library's class |
|
|
164
|
+
| SCRIPT_EXTERNAL/NATIVE_CODE | strip | 90 | `Declare`, `RegisterXLL`, a formula that calls CALL or REGISTER |
|
|
165
|
+
| SCRIPT_EXTERNAL/ENVIRONMENT | strip | 30 | `Environ`, `Command` |
|
|
166
|
+
| SCRIPT_EXTERNAL/REFERENCE | strip | 60 | A VBA project's reference to a library or project outside the list |
|
|
125
167
|
| SCRIPT_DYNAMIC_CODE/EVAL | strip | 80 | `eval`, `Function`, `import()`, FormCalc's `Eval`, `applyXSL` |
|
|
126
168
|
| SCRIPT_DYNAMIC_CODE/TIMER | strip | 70 | `app.setTimeOut`, `setInterval` |
|
|
127
169
|
| SCRIPT_DYNAMIC_CODE/INSTALL_SCRIPT | strip | 70 | `addScript`, `setAction`, `setPageAction`, menu and toolbar items, XFA nodes created to carry script, `loadXML` and `assignNode` outside the data |
|
|
@@ -130,6 +172,7 @@ A form script has nothing to hide, so each of these strips the script:
|
|
|
130
172
|
| SCRIPT_DYNAMIC_CODE/PROTOTYPE | strip | 60 | `constructor`, `__proto__`, `prototype`, `Object.defineProperty`, `Proxy`, `Reflect` |
|
|
131
173
|
| SCRIPT_DYNAMIC_CODE/WITH | strip | 40 | A with statement |
|
|
132
174
|
| SCRIPT_DYNAMIC_CODE/SELF_REFERENCE | strip | 60 | `arguments.callee`, a function's own `toString` |
|
|
175
|
+
| SCRIPT_DYNAMIC_CODE/LEGACY_MACRO | strip | 80 | `WordBasic` |
|
|
133
176
|
| SCRIPT_OBFUSCATION/CHAR_CODES | strip | 50 | `String.fromCharCode` |
|
|
134
177
|
| SCRIPT_OBFUSCATION/DECODE | strip | 60 | Decoders, number bases, text hidden among filler, FormCalc's `Decode` |
|
|
135
178
|
| SCRIPT_OBFUSCATION/CONCATENATION | strip | 40 | A string joined from many short pieces |
|
|
@@ -141,10 +184,12 @@ A form script has nothing to hide, so each of these strips the script:
|
|
|
141
184
|
| SCRIPT_API/EXPLOITED | strip | 80 | `getAnnots`, `syncAnnotScan`, `media`, `spell`, signature calls, `xfa.signature`, `util.printf` with a computed format or a field width past 100 |
|
|
142
185
|
| SCRIPT_API/UNKNOWN | strip | 30 | A viewer API or global name that is not form computation |
|
|
143
186
|
| SCRIPT_API/PASSWORD_PROMPT | strip | 50 | `app.response` or `xfa.host.response` with the password option set |
|
|
144
|
-
| SCRIPT_SYNTAX/UNPARSEABLE | strip | 50 | The script
|
|
187
|
+
| SCRIPT_SYNTAX/UNPARSEABLE | strip | 50 | The script does not parse as JavaScript, as FormCalc for a FormCalc script, or as VBA for a VBA module |
|
|
145
188
|
| SCRIPT_LIMIT/SIZE | strip | 30 | The script is longer than `limits.scriptSize` |
|
|
146
189
|
| SCRIPT_LIMIT/DEPTH | strip | 30 | The script nests deeper than `limits.depth` |
|
|
147
190
|
|
|
191
|
+
The rows above name the PDF APIs. VBA's calls go to the same kinds where they fit: `Workbooks.Open` and `AttachedTemplate` are SCRIPT_EXTERNAL/OPEN_DOCUMENT, `FollowHyperlink` SCRIPT_EXTERNAL/URL, `Application.Run` and `Evaluate` SCRIPT_DYNAMIC_CODE/EVAL, `VBProject` SCRIPT_DYNAMIC_CODE/INSTALL_SCRIPT.
|
|
192
|
+
|
|
148
193
|
The categories start with `SCRIPT_` so they never read as another package's kinds in a shared report. pdf-defuse has a FORM category of its own.
|
|
149
194
|
|
|
150
195
|
Each finding carries the API in `data.api` where there is one, and its `location` gives the line, after the location the container gave. `allFindingSpecs()` returns this table.
|
|
@@ -162,7 +207,7 @@ The score takes the highest weight in full and a quarter of each other kind's we
|
|
|
162
207
|
| `actionOverrides` | none | `[{ category, detail?, action }]` sets a kind to `strip` or `info`. An `info` kind no longer makes a script unsafe |
|
|
163
208
|
| `scoreWeights` | none | `[{ category, detail?, weight }]` |
|
|
164
209
|
| `scoreBands` | above | `{ low, medium, high, critical }` |
|
|
165
|
-
| `context` | none | `inspectScript` only. `{ trigger, location, fieldName, language, form }`.
|
|
210
|
+
| `context` | none | `inspectScript` only. `{ trigger, location, fieldName, language, form, references }`. `location`, or `trigger` without it, goes into the findings' locations. `language: 'formcalc'` reads FormCalc, `language: 'vba'` reads a VBA module, and `form: 'xfa'` reads JavaScript against the XFA object model. `references` are a VBA project's, as `{ name, kind, libid }` |
|
|
166
211
|
| `knownGlobals` | none | `inspectScript` only. Global names scripts that ran before defined and kept |
|
|
167
212
|
|
|
168
213
|
A limit left out is not enforced. `formScriptPlugin(options)` takes the same options but `context` and `knownGlobals`, which it fills in itself, and keeps every decision it made in `plugin.decisions`.
|
|
@@ -172,6 +217,8 @@ A limit left out is not enforced. `formScriptPlugin(options)` takes the same opt
|
|
|
172
217
|
- A form script can loop forever. script-defuse does not decide whether a loop ends.
|
|
173
218
|
- pdf-defuse removes an XFA form's submits, connections, unsafe links and the images no plugin keeps. script-defuse sees only the scripts it is handed.
|
|
174
219
|
- A form that uses a viewer API outside the allowlist loses that script. An action override keeps a kind you accept.
|
|
220
|
+
- In VBA, the list of ways out is a deny list. An Office member that reaches outside the document and is not on it gets through. script-defuse checks the project's references and the classes a module creates, which keeps a library from adding ways the list does not know.
|
|
221
|
+
- A macro that only works on its document can still change or delete what the document holds.
|
|
175
222
|
- JavaScript files, and scripts in HTML, are not covered yet.
|
|
176
223
|
|
|
177
224
|
## Development
|
|
@@ -181,7 +228,7 @@ npm install
|
|
|
181
228
|
npm test
|
|
182
229
|
```
|
|
183
230
|
|
|
184
|
-
`npm test` runs the linter, builds, and runs the tests. The tests build each script they check. No sample from a real form or
|
|
231
|
+
`npm test` runs the linter, builds, and runs the tests. The tests build each script and module they check. No sample from a real form, document or malicious file goes in this repository.
|
|
185
232
|
|
|
186
233
|
## License
|
|
187
234
|
|
package/dist/engine.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export interface InspectScriptOptions extends ScriptOptions {
|
|
|
6
6
|
knownGlobals?: Iterable<string>;
|
|
7
7
|
}
|
|
8
8
|
/**
|
|
9
|
-
* Inspect one script, in JavaScript or, when the context says so, in FormCalc. The verdict is 'form' when no finding strips it: every call and property
|
|
10
|
-
* checks or calculates form fields, and nothing in it hides what it does.
|
|
9
|
+
* Inspect one script, in JavaScript or, when the context says so, in FormCalc or VBA. The verdict is 'form' when no finding strips it: every call and property a form
|
|
10
|
+
* script uses formats, checks or calculates form fields, or a VBA module calls nothing known to reach outside its document, and nothing in it hides what it does.
|
|
11
11
|
*/
|
|
12
12
|
export declare function inspectScript(text: string, options?: InspectScriptOptions): ScriptInspection;
|
package/dist/engine.js
CHANGED
|
@@ -4,14 +4,18 @@ exports.inspectScript = inspectScript;
|
|
|
4
4
|
const analyze_1 = require("./analyze");
|
|
5
5
|
const formcalc_1 = require("./formcalc");
|
|
6
6
|
const score_1 = require("./score");
|
|
7
|
+
const vba_1 = require("./vba");
|
|
7
8
|
/**
|
|
8
|
-
* Inspect one script, in JavaScript or, when the context says so, in FormCalc. The verdict is 'form' when no finding strips it: every call and property
|
|
9
|
-
* checks or calculates form fields, and nothing in it hides what it does.
|
|
9
|
+
* Inspect one script, in JavaScript or, when the context says so, in FormCalc or VBA. The verdict is 'form' when no finding strips it: every call and property a form
|
|
10
|
+
* script uses formats, checks or calculates form fields, or a VBA module calls nothing known to reach outside its document, and nothing in it hides what it does.
|
|
10
11
|
*/
|
|
11
12
|
function inspectScript(text, options = {}) {
|
|
12
|
-
const
|
|
13
|
+
const language = options.context?.language;
|
|
14
|
+
const { findings, definedGlobals } = language === 'formcalc'
|
|
13
15
|
? (0, formcalc_1.analyzeFormCalc)({ text, context: options.context, options })
|
|
14
|
-
:
|
|
16
|
+
: language === 'vba'
|
|
17
|
+
? (0, vba_1.analyzeVba)({ text, context: options.context, options })
|
|
18
|
+
: (0, analyze_1.analyzeScript)({ text, context: options.context, options, known: new Set(options.knownGlobals ?? []) });
|
|
15
19
|
const counted = findings.filter(f => f.action === 'strip');
|
|
16
20
|
const { score, risk } = (0, score_1.scoreFindings)(counted, options);
|
|
17
21
|
return { verdict: counted.length ? 'unsafe' : 'form', score, risk, findings, definedGlobals };
|
package/dist/findings.js
CHANGED
|
@@ -10,6 +10,7 @@ function def(c, d, action, weight, description) {
|
|
|
10
10
|
}
|
|
11
11
|
// FORM
|
|
12
12
|
def(types_1.ScriptCategory.Form, types_1.ScriptDetail.Computation, 'info', 0, 'The script only formats, checks or calculates form fields');
|
|
13
|
+
def(types_1.ScriptCategory.Form, types_1.ScriptDetail.DocumentMacro, 'info', 0, 'The macro calls nothing script-defuse knows to reach outside its document');
|
|
13
14
|
// EXTERNAL
|
|
14
15
|
def(types_1.ScriptCategory.External, types_1.ScriptDetail.Url, 'strip', 60, '<api> opens a web address');
|
|
15
16
|
def(types_1.ScriptCategory.External, types_1.ScriptDetail.Submit, 'strip', 50, '<api> sends the form data somewhere');
|
|
@@ -21,6 +22,14 @@ def(types_1.ScriptCategory.External, types_1.ScriptDetail.Network, 'strip', 70,
|
|
|
21
22
|
def(types_1.ScriptCategory.External, types_1.ScriptDetail.MenuCommand, 'strip', 60, '<api> runs a viewer menu command');
|
|
22
23
|
def(types_1.ScriptCategory.External, types_1.ScriptDetail.Print, 'strip', 20, '<api> prints');
|
|
23
24
|
def(types_1.ScriptCategory.External, types_1.ScriptDetail.Save, 'strip', 30, '<api> saves or closes the document');
|
|
25
|
+
def(types_1.ScriptCategory.External, types_1.ScriptDetail.FileSystem, 'strip', 70, '<api> reads, writes or lists files and folders');
|
|
26
|
+
def(types_1.ScriptCategory.External, types_1.ScriptDetail.Process, 'strip', 90, '<api> starts a program or sends keystrokes to one');
|
|
27
|
+
def(types_1.ScriptCategory.External, types_1.ScriptDetail.Registry, 'strip', 60, '<api> reads or writes the registry or settings files');
|
|
28
|
+
def(types_1.ScriptCategory.External, types_1.ScriptDetail.Dde, 'strip', 80, '<api> talks to another program over DDE');
|
|
29
|
+
def(types_1.ScriptCategory.External, types_1.ScriptDetail.ComObject, 'strip', 80, '<api> creates or gets an object from outside the document, such as a shell or a web request');
|
|
30
|
+
def(types_1.ScriptCategory.External, types_1.ScriptDetail.NativeCode, 'strip', 90, '<api> calls native code in a library');
|
|
31
|
+
def(types_1.ScriptCategory.External, types_1.ScriptDetail.Environment, 'strip', 30, '<api> reads the environment of the computer it runs on');
|
|
32
|
+
def(types_1.ScriptCategory.External, types_1.ScriptDetail.Reference, 'strip', 60, 'The project refers to <api>, a library or project from outside the document');
|
|
24
33
|
// DYNAMIC_CODE
|
|
25
34
|
def(types_1.ScriptCategory.DynamicCode, types_1.ScriptDetail.Eval, 'strip', 80, '<api> runs code built from a string');
|
|
26
35
|
def(types_1.ScriptCategory.DynamicCode, types_1.ScriptDetail.Timer, 'strip', 70, '<api> runs code later, from a string');
|
|
@@ -30,6 +39,7 @@ def(types_1.ScriptCategory.DynamicCode, types_1.ScriptDetail.ComputedAccess, 'st
|
|
|
30
39
|
def(types_1.ScriptCategory.DynamicCode, types_1.ScriptDetail.Prototype, 'strip', 60, '<api> reaches the language machinery behind objects and functions');
|
|
31
40
|
def(types_1.ScriptCategory.DynamicCode, types_1.ScriptDetail.With, 'strip', 40, 'A with statement changes what names refer to');
|
|
32
41
|
def(types_1.ScriptCategory.DynamicCode, types_1.ScriptDetail.SelfReference, 'strip', 60, '<api> reads the source of the running code, the way self-decrypting scripts do');
|
|
42
|
+
def(types_1.ScriptCategory.DynamicCode, types_1.ScriptDetail.LegacyMacro, 'strip', 80, '<api> runs a command of an older macro language, which reaches outside the document');
|
|
33
43
|
// OBFUSCATION
|
|
34
44
|
def(types_1.ScriptCategory.Obfuscation, types_1.ScriptDetail.CharCodes, 'strip', 50, 'Text is built from character codes');
|
|
35
45
|
def(types_1.ScriptCategory.Obfuscation, types_1.ScriptDetail.Decode, 'strip', 60, '<api> decodes hidden text');
|
|
@@ -44,7 +54,7 @@ def(types_1.ScriptCategory.Api, types_1.ScriptDetail.Exploited, 'strip', 80, '<a
|
|
|
44
54
|
def(types_1.ScriptCategory.Api, types_1.ScriptDetail.Unknown, 'strip', 30, '<api> is not a form calculation, format or check');
|
|
45
55
|
def(types_1.ScriptCategory.Api, types_1.ScriptDetail.PasswordPrompt, 'strip', 50, '<api> asks for a password in a prompt, as a fake sign-in does. A form takes a password in a field of its own');
|
|
46
56
|
// SYNTAX
|
|
47
|
-
def(types_1.ScriptCategory.Syntax, types_1.ScriptDetail.Unparseable, 'strip', 50, 'The script
|
|
57
|
+
def(types_1.ScriptCategory.Syntax, types_1.ScriptDetail.Unparseable, 'strip', 50, 'The script does not parse, so what it does cannot be told');
|
|
48
58
|
// LIMIT
|
|
49
59
|
def(types_1.ScriptCategory.Limit, types_1.ScriptDetail.Size, 'strip', 30, 'The script is longer than limits.scriptSize');
|
|
50
60
|
def(types_1.ScriptCategory.Limit, types_1.ScriptDetail.Depth, 'strip', 30, 'The script nests deeper than limits.depth');
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.scoreFindings = exports.DEFAULT_BANDS = exports.COMBINATIONS = exports.formScriptPlugin = exports.analyzeFormCalc = exports.allFindingSpecs = exports.inspectScript = exports.analyzeScript = void 0;
|
|
17
|
+
exports.analyzeVba = exports.scoreFindings = exports.DEFAULT_BANDS = exports.COMBINATIONS = exports.formScriptPlugin = exports.analyzeFormCalc = exports.allFindingSpecs = exports.inspectScript = exports.analyzeScript = void 0;
|
|
18
18
|
var analyze_1 = require("./analyze");
|
|
19
19
|
Object.defineProperty(exports, "analyzeScript", { enumerable: true, get: function () { return analyze_1.analyzeScript; } });
|
|
20
20
|
var engine_1 = require("./engine");
|
|
@@ -30,3 +30,5 @@ Object.defineProperty(exports, "COMBINATIONS", { enumerable: true, get: function
|
|
|
30
30
|
Object.defineProperty(exports, "DEFAULT_BANDS", { enumerable: true, get: function () { return score_1.DEFAULT_BANDS; } });
|
|
31
31
|
Object.defineProperty(exports, "scoreFindings", { enumerable: true, get: function () { return score_1.scoreFindings; } });
|
|
32
32
|
__exportStar(require("./types"), exports);
|
|
33
|
+
var vba_1 = require("./vba");
|
|
34
|
+
Object.defineProperty(exports, "analyzeVba", { enumerable: true, get: function () { return vba_1.analyzeVba; } });
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import cjs from './index.js';
|
|
2
|
-
export const { analyzeScript, inspectScript, allFindingSpecs, analyzeFormCalc, formScriptPlugin, COMBINATIONS, DEFAULT_BANDS, scoreFindings, ScriptCategory, ScriptDetail, ScriptRisk } = cjs;
|
|
2
|
+
export const { analyzeScript, inspectScript, allFindingSpecs, analyzeFormCalc, formScriptPlugin, COMBINATIONS, DEFAULT_BANDS, scoreFindings, analyzeVba, ScriptCategory, ScriptDetail, ScriptRisk } = cjs;
|
|
3
3
|
export default cjs;
|
package/dist/plugins.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.formScriptPlugin = formScriptPlugin;
|
|
4
4
|
const engine_1 = require("./engine");
|
|
5
|
+
const score_1 = require("./score");
|
|
5
6
|
/**
|
|
6
7
|
* A plugin that keeps a script only when inspectScript calls it a form script, and removes every other one.
|
|
7
8
|
*
|
|
@@ -22,8 +23,11 @@ function formScriptPlugin(options = {}) {
|
|
|
22
23
|
if (result === 'passed')
|
|
23
24
|
for (const name of r.definedGlobals)
|
|
24
25
|
known.add(name);
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
// The container scores the script by these weights alone, so they are the ones this script's score counted:
|
|
27
|
+
// a finding that strips, at the caller's weight, and nothing for one that only describes the script.
|
|
28
|
+
const findings = r.findings.map(f => ({ ...f, weight: f.action === 'strip' ? (0, score_1.weightOf)(f, options) : 0 }));
|
|
29
|
+
decisions.push({ script, result, findings });
|
|
30
|
+
return { result, findings };
|
|
27
31
|
},
|
|
28
32
|
};
|
|
29
33
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/** Prefixed with SCRIPT_, so they never read as another package's kinds in a shared report, such as pdf-defuse's FORM. */
|
|
2
2
|
export declare enum ScriptCategory {
|
|
3
|
-
/** The script only formats, checks or calculates form fields. */
|
|
3
|
+
/** The script only formats, checks or calculates form fields, or a macro calls nothing known to reach outside its document. */
|
|
4
4
|
Form = "SCRIPT_FORM",
|
|
5
|
-
/** The script reaches outside the form: a URL, mail, a submit, another file,
|
|
5
|
+
/** The script reaches outside the form or document: a URL, mail, a submit, another file, the network, a program, a library. */
|
|
6
6
|
External = "SCRIPT_EXTERNAL",
|
|
7
7
|
/** The script runs code it builds at run time, installs scripts, or reaches the language's own machinery. */
|
|
8
8
|
DynamicCode = "SCRIPT_DYNAMIC_CODE",
|
|
@@ -15,6 +15,7 @@ export declare enum ScriptCategory {
|
|
|
15
15
|
}
|
|
16
16
|
export declare enum ScriptDetail {
|
|
17
17
|
Computation = "COMPUTATION",
|
|
18
|
+
DocumentMacro = "DOCUMENT_MACRO",
|
|
18
19
|
Url = "URL",
|
|
19
20
|
Submit = "SUBMIT",
|
|
20
21
|
Mail = "MAIL",
|
|
@@ -25,6 +26,14 @@ export declare enum ScriptDetail {
|
|
|
25
26
|
MenuCommand = "MENU_COMMAND",
|
|
26
27
|
Print = "PRINT",
|
|
27
28
|
Save = "SAVE",
|
|
29
|
+
FileSystem = "FILE_SYSTEM",
|
|
30
|
+
Process = "PROCESS",
|
|
31
|
+
Registry = "REGISTRY",
|
|
32
|
+
Dde = "DDE",
|
|
33
|
+
ComObject = "COM_OBJECT",
|
|
34
|
+
NativeCode = "NATIVE_CODE",
|
|
35
|
+
Environment = "ENVIRONMENT",
|
|
36
|
+
Reference = "REFERENCE",
|
|
28
37
|
Eval = "EVAL",
|
|
29
38
|
Timer = "TIMER",
|
|
30
39
|
InstallScript = "INSTALL_SCRIPT",
|
|
@@ -33,6 +42,7 @@ export declare enum ScriptDetail {
|
|
|
33
42
|
Prototype = "PROTOTYPE",
|
|
34
43
|
With = "WITH",
|
|
35
44
|
SelfReference = "SELF_REFERENCE",
|
|
45
|
+
LegacyMacro = "LEGACY_MACRO",
|
|
36
46
|
CharCodes = "CHAR_CODES",
|
|
37
47
|
Decode = "DECODE",
|
|
38
48
|
Concatenation = "CONCATENATION",
|
|
@@ -108,19 +118,28 @@ export interface ScriptOptions {
|
|
|
108
118
|
scoreWeights?: ScriptScoreWeight[];
|
|
109
119
|
scoreBands?: ScriptScoreBands;
|
|
110
120
|
}
|
|
111
|
-
/**
|
|
121
|
+
/** A type library or another VBA project a VBA project refers to, as office-defuse reads it from the dir stream. */
|
|
122
|
+
export interface ScriptReference {
|
|
123
|
+
name: string;
|
|
124
|
+
kind: 'registered' | 'control' | 'project';
|
|
125
|
+
/** A type library's LibidReference, such as `*\G{...}#2.0#0#path#description`, or a project's path. */
|
|
126
|
+
libid: string;
|
|
127
|
+
}
|
|
128
|
+
/** Where a script runs, as a container reports it. pdf-defuse's and office-defuse's ContainedScript fit. */
|
|
112
129
|
export interface ScriptContext {
|
|
113
|
-
/** "document", "open", "field-format", "field-calculate", "click",
|
|
130
|
+
/** "document", "open", "field-format", "field-calculate", "click", ...; for a VBA module "vba-" and its kind. */
|
|
114
131
|
trigger?: string;
|
|
115
132
|
location?: string;
|
|
116
133
|
fieldName?: string;
|
|
117
|
-
/** 'javascript' unless the script is FormCalc, which only XFA forms carry. */
|
|
118
|
-
language?: 'javascript' | 'formcalc';
|
|
119
|
-
/** 'xfa' for a script in an XFA form, which runs against the XFA object model. */
|
|
120
|
-
form?: 'acroform' | 'xfa';
|
|
134
|
+
/** 'javascript' unless the script is FormCalc, which only XFA forms carry, or a VBA module. */
|
|
135
|
+
language?: 'javascript' | 'formcalc' | 'vba';
|
|
136
|
+
/** 'xfa' for a script in an XFA form, which runs against the XFA object model; 'office' for a VBA module. */
|
|
137
|
+
form?: 'acroform' | 'xfa' | 'office';
|
|
138
|
+
/** For a VBA module, the references of its project. office-defuse gives them with the project's first module. */
|
|
139
|
+
references?: ScriptReference[];
|
|
121
140
|
}
|
|
122
141
|
export interface ScriptInspection {
|
|
123
|
-
/** 'form' when no finding strips the script: it only formats, checks or calculates form fields. */
|
|
142
|
+
/** 'form' when no finding strips the script: it only formats, checks or calculates form fields, or a VBA module calls nothing known to reach outside its document. */
|
|
124
143
|
verdict: 'form' | 'unsafe';
|
|
125
144
|
score: number;
|
|
126
145
|
risk: ScriptRisk;
|
package/dist/types.js
CHANGED
|
@@ -4,9 +4,9 @@ exports.ScriptRisk = exports.ScriptDetail = exports.ScriptCategory = void 0;
|
|
|
4
4
|
/** Prefixed with SCRIPT_, so they never read as another package's kinds in a shared report, such as pdf-defuse's FORM. */
|
|
5
5
|
var ScriptCategory;
|
|
6
6
|
(function (ScriptCategory) {
|
|
7
|
-
/** The script only formats, checks or calculates form fields. */
|
|
7
|
+
/** The script only formats, checks or calculates form fields, or a macro calls nothing known to reach outside its document. */
|
|
8
8
|
ScriptCategory["Form"] = "SCRIPT_FORM";
|
|
9
|
-
/** The script reaches outside the form: a URL, mail, a submit, another file,
|
|
9
|
+
/** The script reaches outside the form or document: a URL, mail, a submit, another file, the network, a program, a library. */
|
|
10
10
|
ScriptCategory["External"] = "SCRIPT_EXTERNAL";
|
|
11
11
|
/** The script runs code it builds at run time, installs scripts, or reaches the language's own machinery. */
|
|
12
12
|
ScriptCategory["DynamicCode"] = "SCRIPT_DYNAMIC_CODE";
|
|
@@ -21,6 +21,7 @@ var ScriptDetail;
|
|
|
21
21
|
(function (ScriptDetail) {
|
|
22
22
|
// FORM
|
|
23
23
|
ScriptDetail["Computation"] = "COMPUTATION";
|
|
24
|
+
ScriptDetail["DocumentMacro"] = "DOCUMENT_MACRO";
|
|
24
25
|
// EXTERNAL
|
|
25
26
|
ScriptDetail["Url"] = "URL";
|
|
26
27
|
ScriptDetail["Submit"] = "SUBMIT";
|
|
@@ -32,6 +33,14 @@ var ScriptDetail;
|
|
|
32
33
|
ScriptDetail["MenuCommand"] = "MENU_COMMAND";
|
|
33
34
|
ScriptDetail["Print"] = "PRINT";
|
|
34
35
|
ScriptDetail["Save"] = "SAVE";
|
|
36
|
+
ScriptDetail["FileSystem"] = "FILE_SYSTEM";
|
|
37
|
+
ScriptDetail["Process"] = "PROCESS";
|
|
38
|
+
ScriptDetail["Registry"] = "REGISTRY";
|
|
39
|
+
ScriptDetail["Dde"] = "DDE";
|
|
40
|
+
ScriptDetail["ComObject"] = "COM_OBJECT";
|
|
41
|
+
ScriptDetail["NativeCode"] = "NATIVE_CODE";
|
|
42
|
+
ScriptDetail["Environment"] = "ENVIRONMENT";
|
|
43
|
+
ScriptDetail["Reference"] = "REFERENCE";
|
|
35
44
|
// DYNAMIC_CODE
|
|
36
45
|
ScriptDetail["Eval"] = "EVAL";
|
|
37
46
|
ScriptDetail["Timer"] = "TIMER";
|
|
@@ -41,6 +50,7 @@ var ScriptDetail;
|
|
|
41
50
|
ScriptDetail["Prototype"] = "PROTOTYPE";
|
|
42
51
|
ScriptDetail["With"] = "WITH";
|
|
43
52
|
ScriptDetail["SelfReference"] = "SELF_REFERENCE";
|
|
53
|
+
ScriptDetail["LegacyMacro"] = "LEGACY_MACRO";
|
|
44
54
|
// OBFUSCATION
|
|
45
55
|
ScriptDetail["CharCodes"] = "CHAR_CODES";
|
|
46
56
|
ScriptDetail["Decode"] = "DECODE";
|
package/dist/vba.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ScriptContext, type ScriptFinding, type ScriptOptions } from './types';
|
|
2
|
+
export interface VbaInput {
|
|
3
|
+
text: string;
|
|
4
|
+
context?: ScriptContext;
|
|
5
|
+
options?: ScriptOptions;
|
|
6
|
+
}
|
|
7
|
+
/** Inspects one VBA module. Its findings say what it calls that reaches outside the document, and what it hides. */
|
|
8
|
+
export declare function analyzeVba(input: VbaInput): {
|
|
9
|
+
findings: ScriptFinding[];
|
|
10
|
+
definedGlobals: string[];
|
|
11
|
+
};
|
package/dist/vba.js
ADDED
|
@@ -0,0 +1,728 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.analyzeVba = analyzeVba;
|
|
4
|
+
const analyze_1 = require("./analyze");
|
|
5
|
+
const findings_1 = require("./findings");
|
|
6
|
+
const types_1 = require("./types");
|
|
7
|
+
const EXT = (d) => [types_1.ScriptCategory.External, d];
|
|
8
|
+
const DYN = (d) => [types_1.ScriptCategory.DynamicCode, d];
|
|
9
|
+
/** Names that reach outside the document wherever they appear, bare or as a member. */
|
|
10
|
+
const DENY = new Map([
|
|
11
|
+
// VBA's own library.
|
|
12
|
+
...['kill', 'filecopy', 'mkdir', 'rmdir', 'chdir', 'chdrive', 'setattr', 'dir', 'curdir', 'filelen', 'filedatetime', 'getattr', 'fileattr', 'loadpicture', 'savepicture'].map((n) => [n, EXT(types_1.ScriptDetail.FileSystem)]),
|
|
13
|
+
...['shell', 'sendkeys', 'appactivate', 'macscript', 'applescripttask'].map((n) => [n, EXT(types_1.ScriptDetail.Process)]),
|
|
14
|
+
...['savesetting', 'getsetting', 'getallsettings', 'deletesetting'].map((n) => [n, EXT(types_1.ScriptDetail.Registry)]),
|
|
15
|
+
...['ddeinitiate', 'ddeexecute', 'ddepoke', 'dderequest', 'ddeterminate', 'ddeterminateall'].map((n) => [n, EXT(types_1.ScriptDetail.Dde)]),
|
|
16
|
+
['createobject', EXT(types_1.ScriptDetail.ComObject)],
|
|
17
|
+
['getobject', EXT(types_1.ScriptDetail.ComObject)],
|
|
18
|
+
['callbyname', DYN(types_1.ScriptDetail.ComputedAccess)],
|
|
19
|
+
...['varptr', 'strptr', 'objptr'].map((n) => [n, DYN(types_1.ScriptDetail.Privilege)]),
|
|
20
|
+
['strreverse', [types_1.ScriptCategory.Obfuscation, types_1.ScriptDetail.Decode]],
|
|
21
|
+
// The Office object model.
|
|
22
|
+
...['executeexcel4macro', 'runautomacros'].map((n) => [n, DYN(types_1.ScriptDetail.Eval)]),
|
|
23
|
+
['wordbasic', DYN(types_1.ScriptDetail.LegacyMacro)],
|
|
24
|
+
['ontime', DYN(types_1.ScriptDetail.Timer)],
|
|
25
|
+
...[
|
|
26
|
+
'vbe',
|
|
27
|
+
'vbproject',
|
|
28
|
+
'vbcomponents',
|
|
29
|
+
'codemodule',
|
|
30
|
+
'addfromstring',
|
|
31
|
+
'addfromfile',
|
|
32
|
+
'addfromguid',
|
|
33
|
+
'insertlines',
|
|
34
|
+
'replaceline',
|
|
35
|
+
'deletelines',
|
|
36
|
+
'addins',
|
|
37
|
+
'addins2',
|
|
38
|
+
'comaddins',
|
|
39
|
+
'excel4macrosheets',
|
|
40
|
+
'excel4intlmacrosheets',
|
|
41
|
+
].map((n) => [n, DYN(types_1.ScriptDetail.InstallScript)]),
|
|
42
|
+
['registerxll', EXT(types_1.ScriptDetail.NativeCode)],
|
|
43
|
+
['automationsecurity', DYN(types_1.ScriptDetail.Privilege)],
|
|
44
|
+
...['followhyperlink', 'navigate', 'navigate2'].map((n) => [n, EXT(types_1.ScriptDetail.Url)]),
|
|
45
|
+
...[
|
|
46
|
+
'opentext',
|
|
47
|
+
'openxml',
|
|
48
|
+
'opendatabase',
|
|
49
|
+
'opencurrentdatabase',
|
|
50
|
+
'insertfile',
|
|
51
|
+
'insertfromfile',
|
|
52
|
+
'addpicture',
|
|
53
|
+
'addpicture2',
|
|
54
|
+
'addmediaobject',
|
|
55
|
+
'addmediaobject2',
|
|
56
|
+
'attachedtemplate',
|
|
57
|
+
'sourcefullname',
|
|
58
|
+
'changelink',
|
|
59
|
+
'updatelink',
|
|
60
|
+
'opendatasource',
|
|
61
|
+
'insertdatabase',
|
|
62
|
+
'changefileopendirectory',
|
|
63
|
+
].map((n) => [n, EXT(types_1.ScriptDetail.OpenDocument)]),
|
|
64
|
+
['addoleobject', EXT(types_1.ScriptDetail.DataObject)],
|
|
65
|
+
['querytables', EXT(types_1.ScriptDetail.Network)],
|
|
66
|
+
...['sendmail', 'sendforreview', 'route', 'mailenvelope', 'sendfaxoverinternet'].map((n) => [n, EXT(types_1.ScriptDetail.Mail)]),
|
|
67
|
+
['printout', EXT(types_1.ScriptDetail.Print)],
|
|
68
|
+
...['saveas', 'saveas2', 'savecopyas', 'exportasfixedformat', 'exportasfixedformat2', 'exportasfixedformat3', 'saveasxmldata', 'publishobjects'].map((n) => [n, EXT(types_1.ScriptDetail.Save)]),
|
|
69
|
+
...['importxml', 'xmlimport', 'xmlimportxml', 'exportxml'].map((n) => [n, EXT(types_1.ScriptDetail.ImportExport)]),
|
|
70
|
+
['privateprofilestring', EXT(types_1.ScriptDetail.Registry)],
|
|
71
|
+
['profilestring', EXT(types_1.ScriptDetail.Registry)],
|
|
72
|
+
['activatemicrosoftapp', EXT(types_1.ScriptDetail.Process)],
|
|
73
|
+
// What the objects CreateObject and GetObject return offer. They only come from those, which are denied anyway.
|
|
74
|
+
...['createtextfile', 'opentextfile', 'openastextstream', 'copyfile', 'movefile', 'deletefile', 'copyfolder', 'movefolder', 'deletefolder', 'createfolder', 'savetofile', 'loadfromfile'].map((n) => [n, EXT(types_1.ScriptDetail.FileSystem)]),
|
|
75
|
+
...['exec', 'shellexecute'].map((n) => [n, EXT(types_1.ScriptDetail.Process)]),
|
|
76
|
+
...['regwrite', 'regread', 'regdelete'].map((n) => [n, EXT(types_1.ScriptDetail.Registry)]),
|
|
77
|
+
...['executeglobal', 'executestatement', 'addcode'].map((n) => [n, DYN(types_1.ScriptDetail.Eval)]),
|
|
78
|
+
]);
|
|
79
|
+
/** Names that reach outside only when written bare, since as a member they are common and harmless. */
|
|
80
|
+
const BARE = new Map([
|
|
81
|
+
['environ', EXT(types_1.ScriptDetail.Environment)],
|
|
82
|
+
['command', EXT(types_1.ScriptDetail.Environment)],
|
|
83
|
+
['input', EXT(types_1.ScriptDetail.FileSystem)],
|
|
84
|
+
['tasks', EXT(types_1.ScriptDetail.Process)],
|
|
85
|
+
]);
|
|
86
|
+
/** Names that reach outside only as a member, since bare they are statements or the macro's own names. */
|
|
87
|
+
const MEMBER = new Map([
|
|
88
|
+
['open', EXT(types_1.ScriptDetail.OpenDocument)],
|
|
89
|
+
['follow', EXT(types_1.ScriptDetail.Url)],
|
|
90
|
+
['send', EXT(types_1.ScriptDetail.Mail)],
|
|
91
|
+
['import', EXT(types_1.ScriptDetail.ImportExport)],
|
|
92
|
+
['export', EXT(types_1.ScriptDetail.ImportExport)],
|
|
93
|
+
['compare', EXT(types_1.ScriptDetail.OpenDocument)],
|
|
94
|
+
]);
|
|
95
|
+
/** A member that reaches outside after a particular collection: Hyperlinks.Add adds a link the document did not have. */
|
|
96
|
+
const AFTER = new Map([
|
|
97
|
+
['hyperlinks', new Map([['add', EXT(types_1.ScriptDetail.Url)]])],
|
|
98
|
+
['oleobjects', new Map([['add', EXT(types_1.ScriptDetail.DataObject)]])],
|
|
99
|
+
[
|
|
100
|
+
'connections',
|
|
101
|
+
new Map([
|
|
102
|
+
['add', EXT(types_1.ScriptDetail.Network)],
|
|
103
|
+
['add2', EXT(types_1.ScriptDetail.Network)],
|
|
104
|
+
]),
|
|
105
|
+
],
|
|
106
|
+
['xmlmaps', new Map([['add', EXT(types_1.ScriptDetail.ImportExport)]])],
|
|
107
|
+
['pictures', new Map([['insert', EXT(types_1.ScriptDetail.OpenDocument)]])],
|
|
108
|
+
]);
|
|
109
|
+
/** Collections whose Add opens a template or file when given one. Without arguments, Add makes an empty document. */
|
|
110
|
+
const ADD_WITH_FILE = new Set(['workbooks', 'documents', 'presentations']);
|
|
111
|
+
/** Classes `New` may create: VBA's own, the Scripting runtime's dictionary, VBScript's regular expressions. */
|
|
112
|
+
const NEW_ALLOWED = new Set(['collection', 'dictionary', 'regexp']);
|
|
113
|
+
/** Classes of the allowed libraries that reach outside: the file system object, and another Office instance. */
|
|
114
|
+
const NEW_DENIED = new Map([
|
|
115
|
+
['filesystemobject', EXT(types_1.ScriptDetail.FileSystem)],
|
|
116
|
+
['application', EXT(types_1.ScriptDetail.Process)],
|
|
117
|
+
]);
|
|
118
|
+
/**
|
|
119
|
+
* Type libraries a project may refer to: VBA, OLE Automation, Office, the Office applications, Forms 2.0, the Ref Edit
|
|
120
|
+
* control, the Scripting runtime and VBScript regular expressions. Every other library, and every other project but
|
|
121
|
+
* Word's Normal template, adds code or objects from outside the document.
|
|
122
|
+
*/
|
|
123
|
+
const LIBRARIES = new Set([
|
|
124
|
+
'000204EF-0000-0000-C000-000000000046',
|
|
125
|
+
'00020430-0000-0000-C000-000000000046',
|
|
126
|
+
'2DF8D04C-5BFA-101B-BDE5-00AA0044DE52',
|
|
127
|
+
'00020813-0000-0000-C000-000000000046',
|
|
128
|
+
'00020905-0000-0000-C000-000000000046',
|
|
129
|
+
'91493440-5A91-11CF-8700-00AA0060263B',
|
|
130
|
+
'00021A98-0000-0000-C000-000000000046',
|
|
131
|
+
'0002123C-0000-0000-C000-000000000046',
|
|
132
|
+
'A7107640-94DF-1068-855E-00DD01075445',
|
|
133
|
+
'0D452EE1-E08F-101A-852E-02608C4D0BB4',
|
|
134
|
+
'00024517-0000-0000-C000-000000000046',
|
|
135
|
+
'420B2830-E718-11CF-893D-00A0C9054228',
|
|
136
|
+
'3F4DACA7-160D-11D2-A8E9-00104B365C9F',
|
|
137
|
+
].map(g => g.toUpperCase()));
|
|
138
|
+
/** A worksheet formula that reaches outside when Excel calculates it, by the function it calls. */
|
|
139
|
+
const FORMULA_FUNCTIONS = new Map([
|
|
140
|
+
...['webservice', 'importdata', 'importxml', 'importhtml', 'importfeed', 'importrange', 'rtd'].map((n) => [n, EXT(types_1.ScriptDetail.Network)]),
|
|
141
|
+
...['call', 'register', 'register.id'].map((n) => [n, EXT(types_1.ScriptDetail.NativeCode)]),
|
|
142
|
+
['exec', EXT(types_1.ScriptDetail.Process)],
|
|
143
|
+
['hyperlink', EXT(types_1.ScriptDetail.Url)],
|
|
144
|
+
]);
|
|
145
|
+
const FORMULA_CALL = /([A-Za-z][A-Za-z0-9.]*)\s*\(/g;
|
|
146
|
+
/** A DDE formula, application|topic!item, as `=cmd|' /c calc'!A0`. */
|
|
147
|
+
const DDE_FORMULA = /^[=+\-@][^"]*\w\s*\|[^!]*!/;
|
|
148
|
+
/** A macro name that names another file, as `'other.xlsm'!Macro`, which Excel opens to run it. */
|
|
149
|
+
const EXTERNAL_MACRO = /\.(?:xl[a-z]{1,2}|do[ct][a-z]?|pp[a-z]{1,2})'?!/i;
|
|
150
|
+
class VbaSyntaxError extends Error {
|
|
151
|
+
}
|
|
152
|
+
/** The tokens of a module. A comment that ends in " _" is not taken to go on, so the line after it is read as code. */
|
|
153
|
+
function tokenize(text) {
|
|
154
|
+
const out = [];
|
|
155
|
+
let i = 0;
|
|
156
|
+
let lineStart = true;
|
|
157
|
+
const push = (t, v, pos) => {
|
|
158
|
+
out.push({ t, v, pos });
|
|
159
|
+
lineStart = t === 'nl';
|
|
160
|
+
};
|
|
161
|
+
const idStart = (c) => /[\p{L}_]/u.test(c);
|
|
162
|
+
const idPart = (c) => /[\p{L}\p{N}_]/u.test(c);
|
|
163
|
+
while (i < text.length) {
|
|
164
|
+
const c = text[i];
|
|
165
|
+
if (c === '\r' || c === '\n') {
|
|
166
|
+
if (out.length && out[out.length - 1].t !== 'nl')
|
|
167
|
+
push('nl', '', i);
|
|
168
|
+
lineStart = true;
|
|
169
|
+
i++;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (c === ' ' || c === '\t' || c === '\f' || c === '\v' || c === '\u00a0') {
|
|
173
|
+
i++;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (c === '_' && /^_[ \t]*\r?\n/.test(text.slice(i, i + 64)) && (i === 0 || /[ \t]/.test(text[i - 1]))) {
|
|
177
|
+
// A line continuation: the statement goes on on the next line.
|
|
178
|
+
i = text.indexOf('\n', i) + 1;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (c === "'") {
|
|
182
|
+
while (i < text.length && text[i] !== '\n' && text[i] !== '\r')
|
|
183
|
+
i++;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (c === '"') {
|
|
187
|
+
const start = i;
|
|
188
|
+
let s = '';
|
|
189
|
+
i++;
|
|
190
|
+
for (;;) {
|
|
191
|
+
if (i >= text.length || text[i] === '\n' || text[i] === '\r')
|
|
192
|
+
throw new VbaSyntaxError(`a string that does not end on its line, at offset ${start}`);
|
|
193
|
+
if (text[i] === '"') {
|
|
194
|
+
if (text[i + 1] === '"') {
|
|
195
|
+
s += '"';
|
|
196
|
+
i += 2;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
i++;
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
s += text[i++];
|
|
203
|
+
}
|
|
204
|
+
push('str', s, start);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (c === '[') {
|
|
208
|
+
const end = text.indexOf(']', i);
|
|
209
|
+
const nl = text.slice(i, end < 0 ? text.length : end).search(/[\r\n]/);
|
|
210
|
+
if (end < 0 || nl >= 0)
|
|
211
|
+
throw new VbaSyntaxError(`a bracketed name that does not end on its line, at offset ${i}`);
|
|
212
|
+
push('bracket', text.slice(i + 1, end), i);
|
|
213
|
+
i = end + 1;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (c === '#') {
|
|
217
|
+
const date = /^#[0-9/:\- ,.APMapm]+#/.exec(text.slice(i, i + 64));
|
|
218
|
+
if (date && /\d/.test(date[0])) {
|
|
219
|
+
push('date', date[0], i);
|
|
220
|
+
i += date[0].length;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
push('op', '#', i);
|
|
224
|
+
i++;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (/[0-9]/.test(c) || (c === '.' && /[0-9]/.test(text[i + 1] ?? '')) || (c === '&' && /[hHoO]/.test(text[i + 1] ?? '') && /[0-9a-fA-F]/.test(text[i + 2] ?? ''))) {
|
|
228
|
+
const m = /^(?:&[hH][0-9a-fA-F]+|&[oO][0-7]+|(?:\d+\.?\d*|\.\d+)(?:[eEdD][+-]?\d+)?)[%&!#@^]?/.exec(text.slice(i, i + 64));
|
|
229
|
+
const n = m ? m[0] : c;
|
|
230
|
+
push('num', n, i);
|
|
231
|
+
i += n.length;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (idStart(c)) {
|
|
235
|
+
const start = i;
|
|
236
|
+
while (i < text.length && idPart(text[i]))
|
|
237
|
+
i++;
|
|
238
|
+
const name = text.slice(start, i);
|
|
239
|
+
// A type character: $ always; the others only where no operand follows.
|
|
240
|
+
if (text[i] === '$' || (/[%&!#@]/.test(text[i] ?? '') && !idStart(text[i + 1] ?? '') && !/[0-9"(]/.test(text[i + 1] ?? '')))
|
|
241
|
+
i++;
|
|
242
|
+
const lower = name.toLowerCase();
|
|
243
|
+
if (lower === 'rem' && (lineStart || out[out.length - 1]?.t === 'nl')) {
|
|
244
|
+
while (i < text.length && text[i] !== '\n' && text[i] !== '\r')
|
|
245
|
+
i++;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
push('id', lower, start);
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if (c === ':' && text[i + 1] === '=') {
|
|
252
|
+
push('op', ':=', i);
|
|
253
|
+
i += 2;
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (c === ':') {
|
|
257
|
+
push('nl', ':', i);
|
|
258
|
+
i++;
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
const two = text.slice(i, i + 2);
|
|
262
|
+
if (two === '<>' || two === '<=' || two === '>=') {
|
|
263
|
+
push('op', two, i);
|
|
264
|
+
i += 2;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if ('=<>+-*/\\^&(),.!;'.includes(c)) {
|
|
268
|
+
push('op', c, i);
|
|
269
|
+
i++;
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
throw new VbaSyntaxError(`a character VBA does not have, ${JSON.stringify(c)}, at offset ${i}`);
|
|
273
|
+
}
|
|
274
|
+
return out;
|
|
275
|
+
}
|
|
276
|
+
/** Statement keywords after which a new statement starts on the same line. */
|
|
277
|
+
const STATEMENT_AFTER = new Set(['then', 'else']);
|
|
278
|
+
/** Keywords that may come before Declare and the other statements a rule looks for. */
|
|
279
|
+
const MODIFIERS = new Set(['public', 'private', 'friend', 'static', 'global']);
|
|
280
|
+
/** Statements that declare the names after them. */
|
|
281
|
+
const DECLARES = new Set(['dim', 'const', 'sub', 'function', 'property', 'type', 'enum', 'static', 'redim', 'global', 'public', 'private', 'friend', 'event']);
|
|
282
|
+
/** Keywords inside a declaration that are not names it declares. */
|
|
283
|
+
const DECLARATION_WORDS = new Set([...DECLARES, 'get', 'let', 'set', 'withevents', 'as', 'new', 'optional', 'byval', 'byref', 'paramarray', 'preserve', 'to', 'declare', 'ptrsafe', 'lib', 'alias']);
|
|
284
|
+
/** Where a procedure starts and ends, for names declared inside it. */
|
|
285
|
+
const PROCEDURES = new Set(['sub', 'function', 'property']);
|
|
286
|
+
/** Constants an Add of a new workbook, document or presentation takes in place of a template file. */
|
|
287
|
+
const BUILTIN_CONSTANT = /^(?:xl|wd|pp|mso|vb)/;
|
|
288
|
+
/** Inspects one VBA module. Its findings say what it calls that reaches outside the document, and what it hides. */
|
|
289
|
+
function analyzeVba(input) {
|
|
290
|
+
const options = input.options ?? {};
|
|
291
|
+
const overrides = options.actionOverrides ?? [];
|
|
292
|
+
const findings = [];
|
|
293
|
+
const reported = new Set();
|
|
294
|
+
const lines = [0];
|
|
295
|
+
for (let i = 0; i < input.text.length; i++)
|
|
296
|
+
if (input.text[i] === '\n')
|
|
297
|
+
lines.push(i + 1);
|
|
298
|
+
const lineOf = (pos) => {
|
|
299
|
+
let lo = 0;
|
|
300
|
+
let hi = lines.length - 1;
|
|
301
|
+
while (lo < hi) {
|
|
302
|
+
const mid = (lo + hi + 1) >> 1;
|
|
303
|
+
if (lines[mid] <= pos)
|
|
304
|
+
lo = mid;
|
|
305
|
+
else
|
|
306
|
+
hi = mid - 1;
|
|
307
|
+
}
|
|
308
|
+
return lo + 1;
|
|
309
|
+
};
|
|
310
|
+
const where = (pos) => [input.context?.location ?? input.context?.trigger, pos === undefined ? '' : `line ${lineOf(pos)}`].filter(Boolean).join(', ') || undefined;
|
|
311
|
+
const report = (f, pos, data) => {
|
|
312
|
+
const key = `${f[0]}/${f[1]}/${String(data?.api ?? '')}`;
|
|
313
|
+
if (reported.has(key))
|
|
314
|
+
return;
|
|
315
|
+
reported.add(key);
|
|
316
|
+
findings.push((0, findings_1.makeFinding)(f[0], f[1], overrides, where(pos), data));
|
|
317
|
+
};
|
|
318
|
+
const done = () => {
|
|
319
|
+
if (!findings.some(f => f.action === 'strip'))
|
|
320
|
+
report([types_1.ScriptCategory.Form, types_1.ScriptDetail.DocumentMacro]);
|
|
321
|
+
return { findings, definedGlobals: [] };
|
|
322
|
+
};
|
|
323
|
+
for (const r of input.context?.references ?? [])
|
|
324
|
+
checkReference(r, report);
|
|
325
|
+
const limits = options.limits ?? {};
|
|
326
|
+
if (limits.scriptSize !== undefined && input.text.length > limits.scriptSize) {
|
|
327
|
+
report([types_1.ScriptCategory.Limit, types_1.ScriptDetail.Size], undefined, { length: input.text.length });
|
|
328
|
+
return done();
|
|
329
|
+
}
|
|
330
|
+
let tokens;
|
|
331
|
+
try {
|
|
332
|
+
tokens = tokenize(input.text);
|
|
333
|
+
}
|
|
334
|
+
catch (err) {
|
|
335
|
+
report([types_1.ScriptCategory.Syntax, types_1.ScriptDetail.Unparseable], undefined, { reason: err instanceof Error ? err.message : String(err) });
|
|
336
|
+
return done();
|
|
337
|
+
}
|
|
338
|
+
// Names the module declares at its top and inside each procedure, which a bare name may mean in place of VBA's or
|
|
339
|
+
// Office's. A name declared in one procedure means nothing in another.
|
|
340
|
+
const moduleNames = new Set();
|
|
341
|
+
const scopeOf = new Array(tokens.length);
|
|
342
|
+
{
|
|
343
|
+
let scope = moduleNames;
|
|
344
|
+
let inProcedure = false;
|
|
345
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
346
|
+
const tok = tokens[i];
|
|
347
|
+
const prev = tokens[i - 1];
|
|
348
|
+
const startsStatement = !prev || prev.t === 'nl';
|
|
349
|
+
if (tok.t === 'id' && tok.v === 'end' && tokens[i + 1]?.t === 'id' && PROCEDURES.has(tokens[i + 1].v)) {
|
|
350
|
+
scopeOf[i] = scope;
|
|
351
|
+
inProcedure = false;
|
|
352
|
+
scope = moduleNames;
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
if (startsStatement && tok.t === 'id' && (DECLARES.has(tok.v) || tok.v === 'declare')) {
|
|
356
|
+
let j = i;
|
|
357
|
+
while (tokens[j]?.t === 'id' && DECLARATION_WORDS.has(tokens[j].v))
|
|
358
|
+
j++;
|
|
359
|
+
const procedure = tokens.slice(i, j).some(x => PROCEDURES.has(x.v) && tokens[i].v !== 'declare');
|
|
360
|
+
// The procedure's own name belongs to the module; its parameters and locals to the procedure.
|
|
361
|
+
if (procedure && tokens[j]?.t === 'id')
|
|
362
|
+
moduleNames.add(tokens[j].v);
|
|
363
|
+
if (procedure && !inProcedure) {
|
|
364
|
+
scope = new Set(moduleNames);
|
|
365
|
+
inProcedure = true;
|
|
366
|
+
}
|
|
367
|
+
for (let k = procedure ? j + 1 : j; k < tokens.length && tokens[k].t !== 'nl'; k++) {
|
|
368
|
+
// Every name of a declaration list but types and the parts of qualified names: Dim a, b As Long; Sub F(x As Long).
|
|
369
|
+
const x = tokens[k];
|
|
370
|
+
if (x.t === 'id' && !DECLARATION_WORDS.has(x.v) && tokens[k - 1]?.v !== 'as' && tokens[k - 1]?.v !== '.' && tokens[k - 1]?.v !== 'new')
|
|
371
|
+
scope.add(x.v);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
scopeOf[i] = scope;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
const declaredAt = (pos) => scopeOf[pos] ?? moduleNames;
|
|
378
|
+
const indexOf = new Map(tokens.map((t, i) => [t, i]));
|
|
379
|
+
// Statements, split at line ends, colons, and after Then and Else.
|
|
380
|
+
const statements = [];
|
|
381
|
+
let current = [];
|
|
382
|
+
for (const tok of tokens) {
|
|
383
|
+
if (tok.t === 'nl') {
|
|
384
|
+
if (current.length)
|
|
385
|
+
statements.push(current);
|
|
386
|
+
current = [];
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
current.push(tok);
|
|
390
|
+
if (tok.t === 'id' && STATEMENT_AFTER.has(tok.v)) {
|
|
391
|
+
statements.push(current);
|
|
392
|
+
current = [];
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
if (current.length)
|
|
396
|
+
statements.push(current);
|
|
397
|
+
const appends = new Map();
|
|
398
|
+
for (const st of statements) {
|
|
399
|
+
let k = 0;
|
|
400
|
+
// A line number, as old macros carry, comes before the statement.
|
|
401
|
+
if (st[0].t === 'num')
|
|
402
|
+
k++;
|
|
403
|
+
while (st[k]?.t === 'id' && MODIFIERS.has(st[k].v))
|
|
404
|
+
k++;
|
|
405
|
+
const first = st[k];
|
|
406
|
+
const second = st[k + 1];
|
|
407
|
+
const head = first?.t === 'id' && st[k - 1]?.v !== '.' ? first.v : undefined;
|
|
408
|
+
if (head === 'declare')
|
|
409
|
+
report(EXT(types_1.ScriptDetail.NativeCode), first.pos, { api: `Declare ${second?.t === 'id' ? second.v : ''}`.trim() });
|
|
410
|
+
// The file statements: Open ... For, Print #, Write #, Input #, Line Input #, Get, Put, Lock, Unlock, Seek, Name ... As.
|
|
411
|
+
if (head === 'open' || head === 'print' || head === 'write' || head === 'lock' || head === 'unlock' || (head === 'line' && second?.v === 'input'))
|
|
412
|
+
report(EXT(types_1.ScriptDetail.FileSystem), first.pos, { api: head === 'line' ? 'Line Input' : cap(head) });
|
|
413
|
+
if ((head === 'get' || head === 'put' || head === 'seek' || head === 'width') && second?.v === '#')
|
|
414
|
+
report(EXT(types_1.ScriptDetail.FileSystem), first.pos, { api: cap(head) });
|
|
415
|
+
if ((head === 'get' || head === 'put') && st[k - 1]?.v !== 'property' && second && second.t !== 'id' && second.v !== '(')
|
|
416
|
+
report(EXT(types_1.ScriptDetail.FileSystem), first.pos, { api: cap(head) });
|
|
417
|
+
if (head === 'name' && second && !['=', '.', '(', '!'].includes(second.v) && st.some((x, n) => n > k + 1 && x.t === 'id' && x.v === 'as'))
|
|
418
|
+
report(EXT(types_1.ScriptDetail.FileSystem), first.pos, { api: 'Name' });
|
|
419
|
+
statementRules(st, k, declaredAt(indexOf.get(st[0]) ?? 0), report, appends);
|
|
420
|
+
}
|
|
421
|
+
for (const [name, j] of appends)
|
|
422
|
+
joinedRules(j, undefined, report, name);
|
|
423
|
+
return done();
|
|
424
|
+
}
|
|
425
|
+
const cap = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
426
|
+
function checkReference(r, report) {
|
|
427
|
+
const api = r.name || r.libid.slice(0, 60);
|
|
428
|
+
if (r.kind === 'project') {
|
|
429
|
+
// *\C or *\D, then the path; Word's Normal template is named without one.
|
|
430
|
+
const path = r.libid.replace(/^\*\\[A-Za-z]/, '');
|
|
431
|
+
if (path.toLowerCase() !== 'normal')
|
|
432
|
+
report(EXT(types_1.ScriptDetail.Reference), undefined, { api });
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
const guid = /\{([0-9A-Fa-f-]{36})\}/.exec(r.libid)?.[1]?.toUpperCase();
|
|
436
|
+
if (!guid || !LIBRARIES.has(guid))
|
|
437
|
+
report(EXT(types_1.ScriptDetail.Reference), undefined, { api });
|
|
438
|
+
}
|
|
439
|
+
/** The checks that look at the names, strings and calls of one statement. */
|
|
440
|
+
function statementRules(st, start, declared, report, appends) {
|
|
441
|
+
let charCodes = 0;
|
|
442
|
+
for (let i = start; i < st.length; i++) {
|
|
443
|
+
const tok = st[i];
|
|
444
|
+
const prev = st[i - 1];
|
|
445
|
+
const next = st[i + 1];
|
|
446
|
+
if (tok.t === 'str') {
|
|
447
|
+
stringRules(tok.v, tok.pos, report);
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
if (tok.t === 'bracket') {
|
|
451
|
+
// In Excel, [A1] and [SUM(A1:A3)] are Evaluate written short.
|
|
452
|
+
formulaRules(`=${tok.v}`, tok.pos, report);
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
if (tok.t !== 'id')
|
|
456
|
+
continue;
|
|
457
|
+
const member = prev?.v === '.' || prev?.v === '!';
|
|
458
|
+
const name = tok.v;
|
|
459
|
+
// VBA's and Office's names are taken as theirs even where the module declares the same name, since a declaration
|
|
460
|
+
// in one procedure does not change what the name means in the next.
|
|
461
|
+
const denied = DENY.get(name) ?? (member ? MEMBER.get(name) : declared.has(name) ? undefined : BARE.get(name));
|
|
462
|
+
if (denied)
|
|
463
|
+
report(denied, tok.pos, { api: apiName(st, i) });
|
|
464
|
+
if (member && prev && st[i - 2]?.t === 'id') {
|
|
465
|
+
const before = st[i - 2].v;
|
|
466
|
+
const after = AFTER.get(before)?.get(name);
|
|
467
|
+
if (after)
|
|
468
|
+
report(after, tok.pos, { api: `${cap(before)}.${cap(name)}` });
|
|
469
|
+
if (name === 'add' && ADD_WITH_FILE.has(before) && takesArguments(next, st[i + 2]) && namesFile(st.slice(i + 1)))
|
|
470
|
+
report(EXT(types_1.ScriptDetail.OpenDocument), tok.pos, { api: `${cap(before)}.Add` });
|
|
471
|
+
}
|
|
472
|
+
if (name === 'new' && next?.t === 'id') {
|
|
473
|
+
let j = i + 1;
|
|
474
|
+
while (st[j + 1]?.v === '.' && st[j + 2]?.t === 'id')
|
|
475
|
+
j += 2;
|
|
476
|
+
const cls = st[j].v;
|
|
477
|
+
const deniedClass = NEW_DENIED.get(cls);
|
|
478
|
+
if (deniedClass)
|
|
479
|
+
report(deniedClass, tok.pos, { api: `New ${st[j].v}` });
|
|
480
|
+
else if (j > i + 1 && !NEW_ALLOWED.has(cls))
|
|
481
|
+
report(EXT(types_1.ScriptDetail.ComObject), tok.pos, { api: `New ${st[i + 1].v}.${cls}` });
|
|
482
|
+
}
|
|
483
|
+
if (name === 'addressof')
|
|
484
|
+
report(DYN(types_1.ScriptDetail.Privilege), tok.pos, { api: 'AddressOf' });
|
|
485
|
+
// Run "Macro" and Application.Run name, args run the macro a string names. Run alone, as SlideShowSettings.Run, runs a show.
|
|
486
|
+
if (name === 'run' && (member || !declared.has(name)) && takesArguments(next, st[i + 2]))
|
|
487
|
+
report(DYN(types_1.ScriptDetail.Eval), tok.pos, { api: member ? `${cap(st[i - 2]?.v ?? '')}.Run` : 'Run' });
|
|
488
|
+
if (name === 'evaluate') {
|
|
489
|
+
const arg = next?.v === '(' ? st[i + 2] : next;
|
|
490
|
+
const close = next?.v === '(' ? st[i + 3] : st[i + 2];
|
|
491
|
+
if (arg?.t === 'str' && (!close || close.v === ')' || close.t === 'nl'))
|
|
492
|
+
formulaRules(arg.v.startsWith('=') ? arg.v : `=${arg.v}`, arg.pos, report);
|
|
493
|
+
else
|
|
494
|
+
report(DYN(types_1.ScriptDetail.Eval), tok.pos, { api: 'Evaluate' });
|
|
495
|
+
}
|
|
496
|
+
if (CHR.has(name) && next?.v === '(') {
|
|
497
|
+
const inner = argumentTokens(st, i + 1);
|
|
498
|
+
if (hidesLetter(inner))
|
|
499
|
+
charCodes++;
|
|
500
|
+
const ids = new Set(inner.filter(x => x.t === 'id').map(x => x.v));
|
|
501
|
+
// Chr(Asc(Mid(s, i, 1)) Xor k): a string decoded one character at a time.
|
|
502
|
+
if (ids.has('xor') || ((ids.has('asc') || ids.has('ascw') || ids.has('ascb')) && (ids.has('mid') || ids.has('mid$'))))
|
|
503
|
+
report([types_1.ScriptCategory.Obfuscation, types_1.ScriptDetail.Decode], tok.pos, { api: 'Chr(Asc(...))' });
|
|
504
|
+
}
|
|
505
|
+
if (name === 'replace' && next?.v === '(') {
|
|
506
|
+
const args = splitArguments(argumentTokens(st, i + 1));
|
|
507
|
+
const [text, find, by] = args.map(a => (a.length === 1 && a[0].t === 'str' ? a[0].v : undefined));
|
|
508
|
+
// Replace("pXoXwXeXr", "X", ""): text hidden among filler in a string the macro wrote itself.
|
|
509
|
+
if (text !== undefined && find && by === '' && text.split(find).length - 1 >= 3)
|
|
510
|
+
report([types_1.ScriptCategory.Obfuscation, types_1.ScriptDetail.Decode], tok.pos, { api: 'Replace("...")' });
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
if (charCodes >= 6)
|
|
514
|
+
report([types_1.ScriptCategory.Obfuscation, types_1.ScriptDetail.CharCodes], st[start]?.pos, { api: 'Chr', count: charCodes });
|
|
515
|
+
const chains = literalChains(st, start);
|
|
516
|
+
for (const c of chains)
|
|
517
|
+
if (c.pieces.length > 1)
|
|
518
|
+
joinedRules(c, c.pos, report);
|
|
519
|
+
// x = x & "...": a string built up one piece per statement, judged on what it holds once joined.
|
|
520
|
+
const [target, eq, again, amp] = st.slice(start);
|
|
521
|
+
const first = chains[0];
|
|
522
|
+
if (target?.t === 'id' && eq?.v === '=' && again?.t === 'id' && again.v === target.v && (amp?.v === '&' || amp?.v === '+') && first?.start === start + 4) {
|
|
523
|
+
const j = appends.get(target.v) ?? { pieces: [], pos: target.pos };
|
|
524
|
+
if (j.pieces.length < 100000)
|
|
525
|
+
j.pieces.push(...first.pieces);
|
|
526
|
+
appends.set(target.v, j);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
/** Words of a payload: programs, script hosts, URLs, file types that run, and the objects that fetch and write files. */
|
|
530
|
+
const PAYLOAD_WORDS = [
|
|
531
|
+
'powershell',
|
|
532
|
+
'cmd.exe',
|
|
533
|
+
'cmd /c',
|
|
534
|
+
'wscript',
|
|
535
|
+
'cscript',
|
|
536
|
+
'mshta',
|
|
537
|
+
'rundll32',
|
|
538
|
+
'regsvr32',
|
|
539
|
+
'certutil',
|
|
540
|
+
'bitsadmin',
|
|
541
|
+
'schtasks',
|
|
542
|
+
'wmic',
|
|
543
|
+
'http://',
|
|
544
|
+
'https://',
|
|
545
|
+
'ftp://',
|
|
546
|
+
'.exe',
|
|
547
|
+
'.dll',
|
|
548
|
+
'.vbs',
|
|
549
|
+
'.hta',
|
|
550
|
+
'.ps1',
|
|
551
|
+
'.bat',
|
|
552
|
+
'.scr',
|
|
553
|
+
'.lnk',
|
|
554
|
+
'createobject',
|
|
555
|
+
'getobject',
|
|
556
|
+
'msxml2',
|
|
557
|
+
'xmlhttp',
|
|
558
|
+
'winhttp',
|
|
559
|
+
'adodb',
|
|
560
|
+
'savetofile',
|
|
561
|
+
'responsebody',
|
|
562
|
+
'shell.application',
|
|
563
|
+
'wscript.shell',
|
|
564
|
+
'filesystemobject',
|
|
565
|
+
'winmgmts',
|
|
566
|
+
'win32_process',
|
|
567
|
+
'frombase64string',
|
|
568
|
+
'downloadfile',
|
|
569
|
+
'downloadstring',
|
|
570
|
+
'webclient',
|
|
571
|
+
'invoke-expression',
|
|
572
|
+
'start-process',
|
|
573
|
+
'-encodedcommand',
|
|
574
|
+
'executionpolicy',
|
|
575
|
+
'urldownloadtofile',
|
|
576
|
+
'kernel32',
|
|
577
|
+
'virtualalloc',
|
|
578
|
+
'createthread',
|
|
579
|
+
'rtlmovememory',
|
|
580
|
+
'%temp%',
|
|
581
|
+
'%appdata%',
|
|
582
|
+
];
|
|
583
|
+
/**
|
|
584
|
+
* A joined string is obfuscation when it spells a payload word no piece holds on its own, as "pow" & "ersh" & "ell",
|
|
585
|
+
* and is checked as any string literal is: encoded data, a long blob, a formula.
|
|
586
|
+
*/
|
|
587
|
+
function joinedRules(j, pos, report, api) {
|
|
588
|
+
const text = j.pieces.join('');
|
|
589
|
+
const lower = text.toLowerCase();
|
|
590
|
+
const pieces = j.pieces.map(p => p.toLowerCase());
|
|
591
|
+
const hidden = PAYLOAD_WORDS.find(w => lower.includes(w) && !pieces.some(p => p.includes(w)));
|
|
592
|
+
if (hidden)
|
|
593
|
+
report([types_1.ScriptCategory.Obfuscation, types_1.ScriptDetail.Concatenation], pos ?? j.pos, { count: j.pieces.length, ...(api ? { api } : {}), word: hidden });
|
|
594
|
+
if (j.pieces.length > 1) {
|
|
595
|
+
if (analyze_1.ENCODED.some(re => re.test(text)))
|
|
596
|
+
report([types_1.ScriptCategory.Obfuscation, types_1.ScriptDetail.EncodedString], pos ?? j.pos, { length: text.length });
|
|
597
|
+
else if ((0, analyze_1.isLongBlob)(text))
|
|
598
|
+
report([types_1.ScriptCategory.Obfuscation, types_1.ScriptDetail.LongString], pos ?? j.pos, { length: text.length });
|
|
599
|
+
if (/^[=+\-@]/.test(text))
|
|
600
|
+
formulaRules(text, pos ?? j.pos, report);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
const CHR = new Set(['chr', 'chrw', 'chrb']);
|
|
604
|
+
/**
|
|
605
|
+
* Whether a Chr argument writes a character a string literal could hold as it is, which only hiding it explains:
|
|
606
|
+
* Chr(112) or Chr(115 - 3). Macros write control characters, the quote, the non-breaking space and smart quotes with
|
|
607
|
+
* Chr, and Chr(i) of a variable is a loop's work.
|
|
608
|
+
*/
|
|
609
|
+
function hidesLetter(arg) {
|
|
610
|
+
if (!arg.length || !arg.every(t => t.t === 'num' || (t.t === 'op' && '+-*/\\()'.includes(t.v))))
|
|
611
|
+
return false;
|
|
612
|
+
if (arg.length === 1) {
|
|
613
|
+
const code = numberOf(arg[0].v);
|
|
614
|
+
return code >= 33 && code <= 126 && code !== 34;
|
|
615
|
+
}
|
|
616
|
+
return true;
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* The chains of string literals and Chr calls joined by & or +, as "p" & "ow" & Chr(101) & "r". Pieces joined around
|
|
620
|
+
* variables, as a & "," & b, are separate chains. A Chr of a computed code stands for a character no one can tell.
|
|
621
|
+
*/
|
|
622
|
+
function literalChains(st, start) {
|
|
623
|
+
const chains = [];
|
|
624
|
+
let cur;
|
|
625
|
+
let i = start;
|
|
626
|
+
while (i < st.length) {
|
|
627
|
+
const tok = st[i];
|
|
628
|
+
let end = -1;
|
|
629
|
+
let piece = '';
|
|
630
|
+
if (tok.t === 'str') {
|
|
631
|
+
end = i + 1;
|
|
632
|
+
piece = tok.v;
|
|
633
|
+
}
|
|
634
|
+
else if (tok.t === 'id' && CHR.has(tok.v) && st[i + 1]?.v === '(') {
|
|
635
|
+
const arg = argumentTokens(st, i + 1);
|
|
636
|
+
end = i + 2 + arg.length + 1;
|
|
637
|
+
const code = arg.length === 1 && arg[0].t === 'num' ? numberOf(arg[0].v) : undefined;
|
|
638
|
+
piece = code !== undefined && code >= 0 && code < 0x110000 ? String.fromCodePoint(code) : '\ufffd';
|
|
639
|
+
}
|
|
640
|
+
if (end < 0) {
|
|
641
|
+
cur = undefined;
|
|
642
|
+
i++;
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
if (!cur) {
|
|
646
|
+
cur = { pieces: [], pos: tok.pos, start: i };
|
|
647
|
+
chains.push(cur);
|
|
648
|
+
}
|
|
649
|
+
cur.pieces.push(piece);
|
|
650
|
+
if (st[end]?.v === '&' || st[end]?.v === '+')
|
|
651
|
+
i = end + 1;
|
|
652
|
+
else {
|
|
653
|
+
cur = undefined;
|
|
654
|
+
i = end;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
return chains;
|
|
658
|
+
}
|
|
659
|
+
/** A VBA number literal's value: decimal, &H hex or &O octal, with any type character. */
|
|
660
|
+
function numberOf(v) {
|
|
661
|
+
const m = /^&([hHoO])([0-9a-fA-F]+)/.exec(v);
|
|
662
|
+
return m ? Number.parseInt(m[2], /[hH]/.test(m[1]) ? 16 : 8) : Number.parseFloat(v);
|
|
663
|
+
}
|
|
664
|
+
/** Whether the arguments of an Add name a file or template: anything but Office's constants, numbers and named-argument names. */
|
|
665
|
+
function namesFile(args) {
|
|
666
|
+
return args.some((t, k) => (t.t === 'str' && t.v !== '') || (t.t === 'id' && !BUILTIN_CONSTANT.test(t.v) && args[k + 1]?.v !== ':=' && !['true', 'false'].includes(t.v)));
|
|
667
|
+
}
|
|
668
|
+
/** Whether a call's next tokens pass it arguments: anything but the end of the statement, "=", ")" or ",". */
|
|
669
|
+
function takesArguments(next, after) {
|
|
670
|
+
if (!next || next.t === 'nl')
|
|
671
|
+
return false;
|
|
672
|
+
if (next.v === '(')
|
|
673
|
+
return after !== undefined && after.v !== ')';
|
|
674
|
+
if (next.t === 'op' && ['=', ')', ',', '.', ':=', '&', '+', '-', '*', '/', '<>', '<', '>', '<=', '>='].includes(next.v))
|
|
675
|
+
return false;
|
|
676
|
+
return !(next.t === 'id' && ['then', 'else', 'and', 'or', 'xor', 'mod', 'is', 'to', 'step'].includes(next.v));
|
|
677
|
+
}
|
|
678
|
+
/** The tokens inside the parentheses that open at `open`. */
|
|
679
|
+
function argumentTokens(st, open) {
|
|
680
|
+
let depth = 0;
|
|
681
|
+
for (let j = open; j < st.length; j++) {
|
|
682
|
+
if (st[j].v === '(' && st[j].t === 'op')
|
|
683
|
+
depth++;
|
|
684
|
+
else if (st[j].v === ')' && st[j].t === 'op' && --depth === 0)
|
|
685
|
+
return st.slice(open + 1, j);
|
|
686
|
+
}
|
|
687
|
+
return st.slice(open + 1);
|
|
688
|
+
}
|
|
689
|
+
function splitArguments(tokens) {
|
|
690
|
+
const args = [[]];
|
|
691
|
+
let depth = 0;
|
|
692
|
+
for (const tok of tokens) {
|
|
693
|
+
if (tok.t === 'op' && tok.v === '(')
|
|
694
|
+
depth++;
|
|
695
|
+
if (tok.t === 'op' && tok.v === ')')
|
|
696
|
+
depth--;
|
|
697
|
+
if (tok.t === 'op' && tok.v === ',' && depth === 0)
|
|
698
|
+
args.push([]);
|
|
699
|
+
else
|
|
700
|
+
args[args.length - 1].push(tok);
|
|
701
|
+
}
|
|
702
|
+
return args;
|
|
703
|
+
}
|
|
704
|
+
/** The name a finding shows: the member with what it is a member of, as Workbooks.Open. */
|
|
705
|
+
function apiName(st, i) {
|
|
706
|
+
const own = cap(st[i].v);
|
|
707
|
+
return (st[i - 1]?.v === '.' || st[i - 1]?.v === '!') && st[i - 2]?.t === 'id' ? `${cap(st[i - 2].v)}.${own}` : own;
|
|
708
|
+
}
|
|
709
|
+
function stringRules(s, pos, report) {
|
|
710
|
+
if (analyze_1.ENCODED.some(re => re.test(s)))
|
|
711
|
+
report([types_1.ScriptCategory.Obfuscation, types_1.ScriptDetail.EncodedString], pos, { length: s.length });
|
|
712
|
+
else if ((0, analyze_1.isLongBlob)(s))
|
|
713
|
+
report([types_1.ScriptCategory.Obfuscation, types_1.ScriptDetail.LongString], pos, { length: s.length });
|
|
714
|
+
if (/^[=+\-@]/.test(s))
|
|
715
|
+
formulaRules(s, pos, report);
|
|
716
|
+
if (EXTERNAL_MACRO.test(s))
|
|
717
|
+
report(EXT(types_1.ScriptDetail.OpenDocument), pos, { api: 'a macro in another file' });
|
|
718
|
+
}
|
|
719
|
+
/** A formula a macro writes into a cell or evaluates, checked for the functions and DDE links that reach outside. */
|
|
720
|
+
function formulaRules(formula, pos, report) {
|
|
721
|
+
if (DDE_FORMULA.test(formula))
|
|
722
|
+
report(EXT(types_1.ScriptDetail.Dde), pos, { api: 'a DDE formula' });
|
|
723
|
+
for (const m of formula.matchAll(FORMULA_CALL)) {
|
|
724
|
+
const f = FORMULA_FUNCTIONS.get(m[1].toLowerCase().replace(/^_xlfn\./, ''));
|
|
725
|
+
if (f)
|
|
726
|
+
report(f, pos, { api: m[1].toUpperCase() });
|
|
727
|
+
}
|
|
728
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@patrickdk77/script-defuse",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Inspect PDF form scripts in JavaScript and FormCalc, from AcroForm and XFA forms. Keeps a script only when every call and property it uses formats, checks or calculates form fields, and
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Inspect document scripts: PDF form scripts in JavaScript and FormCalc, from AcroForm and XFA forms, and the VBA modules of Office documents. Keeps a form script only when every call and property it uses formats, checks or calculates form fields, and a macro only when it calls nothing that reaches outside its document. Reports external access, dynamic code and obfuscation. Includes a script plugin for pdf-defuse and office-defuse.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "patrickdk77",
|
|
7
7
|
"main": "./dist/index.js",
|