@patrickdk77/script-defuse 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +187 -2
- package/dist/analyze.d.ts +22 -0
- package/dist/analyze.js +770 -0
- package/dist/api.d.ts +82 -0
- package/dist/api.js +698 -0
- package/dist/engine.d.ts +12 -0
- package/dist/engine.js +18 -0
- package/dist/findings.d.ts +19 -0
- package/dist/findings.js +79 -0
- package/dist/formcalc.d.ts +10 -0
- package/dist/formcalc.js +792 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +32 -0
- package/dist/index.mjs +3 -0
- package/dist/plugins.d.ts +28 -0
- package/dist/plugins.js +29 -0
- package/dist/score.d.ts +17 -0
- package/dist/score.js +51 -0
- package/dist/types.d.ts +130 -0
- package/dist/types.js +70 -0
- package/package.json +84 -4
package/dist/analyze.js
ADDED
|
@@ -0,0 +1,770 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ENCODED = void 0;
|
|
4
|
+
exports.analyzeScript = analyzeScript;
|
|
5
|
+
exports.isLongBlob = isLongBlob;
|
|
6
|
+
const acorn_1 = require("acorn");
|
|
7
|
+
const api_1 = require("./api");
|
|
8
|
+
const findings_1 = require("./findings");
|
|
9
|
+
const types_1 = require("./types");
|
|
10
|
+
const PLAIN = new Set(['data', 'string', 'number']);
|
|
11
|
+
const HOST_TABLES = new Map([
|
|
12
|
+
['doc', api_1.DOC],
|
|
13
|
+
['field', api_1.FIELD],
|
|
14
|
+
['event', api_1.EVENT],
|
|
15
|
+
['app', api_1.APP],
|
|
16
|
+
['util', api_1.UTIL],
|
|
17
|
+
['color', api_1.COLOR],
|
|
18
|
+
['console', api_1.CONSOLE],
|
|
19
|
+
['xmldata', api_1.XMLDATA],
|
|
20
|
+
['xfa', api_1.XFA_ROOT],
|
|
21
|
+
['xfahost', api_1.XFA_HOST],
|
|
22
|
+
['xfalayout', api_1.XFA_LAYOUT],
|
|
23
|
+
['xfaevent', api_1.XFA_EVENT],
|
|
24
|
+
]);
|
|
25
|
+
const ACROFORM_ROOTS = new Map([
|
|
26
|
+
['event', 'event'],
|
|
27
|
+
['app', 'app'],
|
|
28
|
+
['util', 'util'],
|
|
29
|
+
['color', 'color'],
|
|
30
|
+
['console', 'console'],
|
|
31
|
+
['XMLData', 'xmldata'],
|
|
32
|
+
]);
|
|
33
|
+
/** An XFA script sees Acrobat's objects too, but its event is the XFA event and `xfa` is the form's object model. */
|
|
34
|
+
const XFA_ROOTS = new Map([
|
|
35
|
+
...ACROFORM_ROOTS,
|
|
36
|
+
['event', 'xfaevent'],
|
|
37
|
+
['xfa', 'xfa'],
|
|
38
|
+
...[...api_1.SOM_ROOTS].flatMap(([name, v]) => (typeof v === 'string' ? [[name, v]] : [])),
|
|
39
|
+
]);
|
|
40
|
+
const VALUES = new Set(['undefined', 'NaN', 'Infinity', 'arguments']);
|
|
41
|
+
/** Acrobat objects an XFA form's script object of the same name may stand for. */
|
|
42
|
+
const SHADOWED = new Set(['util', 'color', 'console']);
|
|
43
|
+
/** Node methods that change the form's own trees, which only data may go through. */
|
|
44
|
+
const DATA_ONLY = new Set(['loadXML', 'assignNode']);
|
|
45
|
+
const DATA_PATH = /^(?:xfa\.(?:datasets|data|record)|\$data|\$record|!)/;
|
|
46
|
+
const ARITHMETIC = new Set(['-', '*', '/', '%', '**', '|', '&', '^', '<<', '>>', '>>>']);
|
|
47
|
+
/** A string of base64, hex, or %u and \x escapes long enough to carry a payload. */
|
|
48
|
+
exports.ENCODED = [/^[A-Za-z0-9+/]{120,}={0,2}$/, /^(?:[0-9a-fA-F]{2}){60,}$/, /(?:%u[0-9a-fA-F]{4}){8,}/, /(?:%[0-9a-fA-F]{2}){30,}/, /(?:\\x[0-9a-fA-F]{2}){20,}/];
|
|
49
|
+
/** An escape that writes a printable ASCII character, which a script has no reason to escape. */
|
|
50
|
+
const NEEDLESS_ESCAPE = /\\x(?:[2-7][0-9a-fA-F])|\\u00(?:[2-7][0-9a-fA-F])/g;
|
|
51
|
+
class Scope {
|
|
52
|
+
parent;
|
|
53
|
+
bindings = new Map();
|
|
54
|
+
constructor(parent) {
|
|
55
|
+
this.parent = parent;
|
|
56
|
+
}
|
|
57
|
+
lookup(name) {
|
|
58
|
+
for (let s = this; s; s = s.parent) {
|
|
59
|
+
const b = s.bindings.get(name);
|
|
60
|
+
if (b)
|
|
61
|
+
return b;
|
|
62
|
+
}
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
declare(name, param = false) {
|
|
66
|
+
const found = this.bindings.get(name);
|
|
67
|
+
if (found)
|
|
68
|
+
return found;
|
|
69
|
+
const b = { name, kinds: new Set(), sources: [], param };
|
|
70
|
+
this.bindings.set(name, b);
|
|
71
|
+
return b;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function isNode(v) {
|
|
75
|
+
return typeof v === 'object' && v !== null && typeof v.type === 'string';
|
|
76
|
+
}
|
|
77
|
+
function children(n) {
|
|
78
|
+
const out = [];
|
|
79
|
+
for (const [key, v] of Object.entries(n)) {
|
|
80
|
+
if (key === 'loc' || key === 'range')
|
|
81
|
+
continue;
|
|
82
|
+
if (Array.isArray(v)) {
|
|
83
|
+
for (const x of v)
|
|
84
|
+
if (isNode(x))
|
|
85
|
+
out.push(x);
|
|
86
|
+
}
|
|
87
|
+
else if (isNode(v))
|
|
88
|
+
out.push(v);
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
const isFunction = (n) => n.type === 'FunctionDeclaration' || n.type === 'FunctionExpression' || n.type === 'ArrowFunctionExpression';
|
|
93
|
+
function patternNames(p, out = []) {
|
|
94
|
+
if (p.type === 'Identifier')
|
|
95
|
+
out.push(p.name);
|
|
96
|
+
else if (p.type === 'ObjectPattern')
|
|
97
|
+
for (const prop of p.properties)
|
|
98
|
+
patternNames(prop.type === 'RestElement' ? prop.argument : prop.value, out);
|
|
99
|
+
else if (p.type === 'ArrayPattern') {
|
|
100
|
+
for (const e of p.elements)
|
|
101
|
+
if (e)
|
|
102
|
+
patternNames(e, out);
|
|
103
|
+
}
|
|
104
|
+
else if (p.type === 'RestElement')
|
|
105
|
+
patternNames(p.argument, out);
|
|
106
|
+
else if (p.type === 'AssignmentPattern')
|
|
107
|
+
patternNames(p.left, out);
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
function analyzeScript(input) {
|
|
111
|
+
const options = input.options ?? {};
|
|
112
|
+
const xfa = input.context?.form === 'xfa';
|
|
113
|
+
const roots = xfa ? XFA_ROOTS : ACROFORM_ROOTS;
|
|
114
|
+
const overrides = options.actionOverrides ?? [];
|
|
115
|
+
const known = input.known ?? new Set();
|
|
116
|
+
const findings = [];
|
|
117
|
+
const reported = new Set();
|
|
118
|
+
const where = (n) => {
|
|
119
|
+
const line = n ? `line ${lineOf(input.text, n.start)}` : '';
|
|
120
|
+
const ctx = input.context?.location ?? (input.context?.fieldName ? `field ${JSON.stringify(input.context.fieldName)}` : input.context?.trigger);
|
|
121
|
+
return [ctx, line].filter(Boolean).join(', ') || undefined;
|
|
122
|
+
};
|
|
123
|
+
const report = (c, d, n, data) => {
|
|
124
|
+
const key = `${c}/${d}/${String(data?.api ?? '')}`;
|
|
125
|
+
if (reported.has(key))
|
|
126
|
+
return;
|
|
127
|
+
reported.add(key);
|
|
128
|
+
findings.push((0, findings_1.makeFinding)(c, d, overrides, where(n), data));
|
|
129
|
+
};
|
|
130
|
+
const reportMember = (m, api, n) => {
|
|
131
|
+
if ('finding' in m)
|
|
132
|
+
report(m.finding[0], m.finding[1], n, { api });
|
|
133
|
+
};
|
|
134
|
+
const limits = options.limits ?? {};
|
|
135
|
+
if (limits.scriptSize !== undefined && input.text.length > limits.scriptSize) {
|
|
136
|
+
report(types_1.ScriptCategory.Limit, types_1.ScriptDetail.Size, undefined, { length: input.text.length });
|
|
137
|
+
return { findings, definedGlobals: [] };
|
|
138
|
+
}
|
|
139
|
+
let program;
|
|
140
|
+
try {
|
|
141
|
+
program = (0, acorn_1.parse)(input.text, { ecmaVersion: 'latest', sourceType: 'script', allowReturnOutsideFunction: true, allowHashBang: true });
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
report(types_1.ScriptCategory.Syntax, types_1.ScriptDetail.Unparseable, undefined, { reason: err instanceof Error ? err.message : String(err) });
|
|
145
|
+
return { findings, definedGlobals: [] };
|
|
146
|
+
}
|
|
147
|
+
// Nesting, checked before anything recursive walks the tree.
|
|
148
|
+
if (limits.depth !== undefined) {
|
|
149
|
+
let deepest = 0;
|
|
150
|
+
const stack = [[program, 1]];
|
|
151
|
+
while (stack.length) {
|
|
152
|
+
const [n, d] = stack.pop();
|
|
153
|
+
deepest = Math.max(deepest, d);
|
|
154
|
+
if (d > limits.depth)
|
|
155
|
+
break;
|
|
156
|
+
for (const c of children(n))
|
|
157
|
+
stack.push([c, d + 1]);
|
|
158
|
+
}
|
|
159
|
+
if (deepest > limits.depth) {
|
|
160
|
+
report(types_1.ScriptCategory.Limit, types_1.ScriptDetail.Depth, undefined, { depth: limits.depth });
|
|
161
|
+
return { findings, definedGlobals: [] };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const top = new Scope();
|
|
165
|
+
const scopeOf = new Map();
|
|
166
|
+
// 1. Declarations: functions, parameters, var, let, const and catch parameters, hoisted to their function.
|
|
167
|
+
const declare = (n, scope) => {
|
|
168
|
+
if (isFunction(n)) {
|
|
169
|
+
if (n.type === 'FunctionDeclaration' && n.id) {
|
|
170
|
+
const b = scope.declare(n.id.name);
|
|
171
|
+
b.kinds.add('data');
|
|
172
|
+
b.fn = true;
|
|
173
|
+
}
|
|
174
|
+
const inner = new Scope(scope);
|
|
175
|
+
scopeOf.set(n, inner);
|
|
176
|
+
if (n.type === 'FunctionExpression' && n.id)
|
|
177
|
+
inner.declare(n.id.name).kinds.add('data');
|
|
178
|
+
for (const p of n.params)
|
|
179
|
+
for (const name of patternNames(p))
|
|
180
|
+
inner.declare(name, true).kinds.add('unknown');
|
|
181
|
+
declare(n.body, inner);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (n.type === 'VariableDeclaration')
|
|
185
|
+
for (const d of n.declarations)
|
|
186
|
+
for (const name of patternNames(d.id))
|
|
187
|
+
scope.declare(name);
|
|
188
|
+
if (n.type === 'CatchClause' && n.param)
|
|
189
|
+
for (const name of patternNames(n.param))
|
|
190
|
+
scope.declare(name).kinds.add('unknown');
|
|
191
|
+
if (n.type === 'ClassDeclaration' && n.id)
|
|
192
|
+
scope.declare(n.id.name).kinds.add('data');
|
|
193
|
+
for (const c of children(n))
|
|
194
|
+
declare(c, scope);
|
|
195
|
+
};
|
|
196
|
+
declare(program, top);
|
|
197
|
+
// 2. What each binding is given: initializers and assignments. An assignment to a name nothing declares makes a
|
|
198
|
+
// global, unless it is a property of the document, which an AcroForm script's bare names reach.
|
|
199
|
+
const global = (name) => (!xfa && api_1.DOC_ALL.has(name) ? undefined : top.declare(name));
|
|
200
|
+
const assign = (target, expr, scope) => {
|
|
201
|
+
for (const name of patternNames(target)) {
|
|
202
|
+
const b = scope.lookup(name) ?? global(name);
|
|
203
|
+
if (!b)
|
|
204
|
+
continue;
|
|
205
|
+
if (expr && target.type === 'Identifier')
|
|
206
|
+
b.sources.push({ expr, scope });
|
|
207
|
+
else
|
|
208
|
+
b.kinds.add('unknown');
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
const collect = (n, scope) => {
|
|
212
|
+
const inner = scopeOf.get(n) ?? scope;
|
|
213
|
+
// A declaration without a value holds undefined until an assignment gives it one, so it adds no kind.
|
|
214
|
+
if (n.type === 'VariableDeclarator') {
|
|
215
|
+
if (n.init || n.id.type !== 'Identifier')
|
|
216
|
+
assign(n.id, n.init ?? undefined, scope);
|
|
217
|
+
}
|
|
218
|
+
else if (!xfa && n.type === 'AssignmentExpression' && n.left.type === 'MemberExpression' && n.left.object.type === 'ThisExpression') {
|
|
219
|
+
// A property set on the document is a global the viewer resolves unqualified names to.
|
|
220
|
+
const name = memberName(n.left);
|
|
221
|
+
if (name !== undefined && !api_1.DOC_ALL.has(name)) {
|
|
222
|
+
const b = top.declare(name);
|
|
223
|
+
if (n.operator === '=')
|
|
224
|
+
b.sources.push({ expr: n.right, scope });
|
|
225
|
+
else
|
|
226
|
+
b.kinds.add(n.operator === '+=' ? 'data' : 'number');
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
else if (n.type === 'AssignmentExpression' && n.left.type !== 'MemberExpression') {
|
|
230
|
+
if (n.operator === '=')
|
|
231
|
+
assign(n.left, n.right, scope);
|
|
232
|
+
else
|
|
233
|
+
for (const name of patternNames(n.left))
|
|
234
|
+
(scope.lookup(name) ?? global(name))?.kinds.add(n.operator === '+=' ? 'data' : 'number');
|
|
235
|
+
}
|
|
236
|
+
else if ((n.type === 'ForInStatement' || n.type === 'ForOfStatement') && n.left.type !== 'VariableDeclaration' && n.left.type !== 'MemberExpression') {
|
|
237
|
+
for (const name of patternNames(n.left))
|
|
238
|
+
(scope.lookup(name) ?? global(name))?.kinds.add(n.type === 'ForInStatement' ? 'string' : 'unknown');
|
|
239
|
+
}
|
|
240
|
+
else if ((n.type === 'ForInStatement' || n.type === 'ForOfStatement') && n.left.type === 'VariableDeclaration') {
|
|
241
|
+
for (const d of n.left.declarations)
|
|
242
|
+
for (const name of patternNames(d.id))
|
|
243
|
+
(scope.lookup(name) ?? scope.declare(name)).kinds.add(n.type === 'ForInStatement' ? 'string' : 'unknown');
|
|
244
|
+
}
|
|
245
|
+
else if (n.type === 'UpdateExpression' && n.argument.type === 'Identifier')
|
|
246
|
+
(scope.lookup(n.argument.name) ?? global(n.argument.name))?.kinds.add('number');
|
|
247
|
+
for (const c of children(n))
|
|
248
|
+
collect(c, inner);
|
|
249
|
+
};
|
|
250
|
+
collect(program, top);
|
|
251
|
+
// 3. Kinds, to a fixpoint: a binding is everything it is ever given.
|
|
252
|
+
const kindOf = (e, scope) => {
|
|
253
|
+
switch (e.type) {
|
|
254
|
+
case 'Literal':
|
|
255
|
+
return new Set([typeof e.value === 'number' ? 'number' : typeof e.value === 'string' ? 'string' : 'data']);
|
|
256
|
+
case 'TemplateLiteral':
|
|
257
|
+
return new Set(['string']);
|
|
258
|
+
case 'ArrayExpression':
|
|
259
|
+
case 'ObjectExpression':
|
|
260
|
+
case 'FunctionExpression':
|
|
261
|
+
case 'ArrowFunctionExpression':
|
|
262
|
+
case 'ClassExpression':
|
|
263
|
+
case 'NewExpression':
|
|
264
|
+
return new Set(['data']);
|
|
265
|
+
case 'ThisExpression':
|
|
266
|
+
return new Set([xfa ? 'node' : 'doc']);
|
|
267
|
+
case 'Identifier': {
|
|
268
|
+
const b = scope.lookup(e.name);
|
|
269
|
+
if (b)
|
|
270
|
+
return new Set(b.kinds);
|
|
271
|
+
const root = roots.get(e.name);
|
|
272
|
+
if (root)
|
|
273
|
+
return new Set([root]);
|
|
274
|
+
if (api_1.CONSTANT_ROOTS.has(e.name))
|
|
275
|
+
return new Set(['constant']);
|
|
276
|
+
if (e.name === 'Math')
|
|
277
|
+
return new Set(['math']);
|
|
278
|
+
if (api_1.BUILTIN_NAMESPACES.has(e.name))
|
|
279
|
+
return new Set(['builtin']);
|
|
280
|
+
if (VALUES.has(e.name) || api_1.AFORM.has(e.name) || known.has(e.name) || api_1.BUILTIN_FUNCTIONS.has(e.name))
|
|
281
|
+
return new Set(['data']);
|
|
282
|
+
// An XFA script reaches the form's nodes by name.
|
|
283
|
+
if (xfa)
|
|
284
|
+
return new Set(['node']);
|
|
285
|
+
const docMember = api_1.DOC.get(e.name);
|
|
286
|
+
if (docMember && 'kind' in docMember)
|
|
287
|
+
return new Set([docMember.kind]);
|
|
288
|
+
return new Set(['unknown']);
|
|
289
|
+
}
|
|
290
|
+
case 'MemberExpression': {
|
|
291
|
+
const obj = kindOf(e.object, scope);
|
|
292
|
+
const name = memberName(e);
|
|
293
|
+
const out = new Set();
|
|
294
|
+
for (const k of obj) {
|
|
295
|
+
const t = HOST_TABLES.get(k);
|
|
296
|
+
const own = k === 'doc' && name !== undefined && !api_1.DOC.has(name) ? top.bindings.get(name) : undefined;
|
|
297
|
+
if (own)
|
|
298
|
+
for (const x of own.kinds)
|
|
299
|
+
out.add(x);
|
|
300
|
+
else if (t && name !== undefined) {
|
|
301
|
+
const m = t.get(name);
|
|
302
|
+
out.add(m && 'kind' in m ? m.kind : xfa && SHADOWED.has(k) ? 'node' : 'unknown');
|
|
303
|
+
}
|
|
304
|
+
else if (k === 'fields')
|
|
305
|
+
out.add(name === 'length' ? 'number' : 'field');
|
|
306
|
+
else if (k === 'node')
|
|
307
|
+
out.add((name !== undefined && api_1.NODE_VALUES.get(name)) || 'node');
|
|
308
|
+
else if (k === 'constant' || k === 'math')
|
|
309
|
+
out.add(name === undefined ? 'unknown' : 'data');
|
|
310
|
+
else if (k === 'builtin')
|
|
311
|
+
out.add('data');
|
|
312
|
+
else if (name === 'length')
|
|
313
|
+
out.add('number');
|
|
314
|
+
else
|
|
315
|
+
out.add('unknown');
|
|
316
|
+
}
|
|
317
|
+
return out;
|
|
318
|
+
}
|
|
319
|
+
case 'CallExpression': {
|
|
320
|
+
if (e.callee.type === 'MemberExpression')
|
|
321
|
+
return kindOf(e.callee, scope);
|
|
322
|
+
if (e.callee.type === 'Identifier') {
|
|
323
|
+
const builtin = api_1.BUILTIN_FUNCTIONS.get(e.callee.name);
|
|
324
|
+
if (builtin)
|
|
325
|
+
return new Set([builtin]);
|
|
326
|
+
const docMember = !xfa && !scope.lookup(e.callee.name) && api_1.DOC.get(e.callee.name);
|
|
327
|
+
if (docMember && 'kind' in docMember)
|
|
328
|
+
return new Set([docMember.kind]);
|
|
329
|
+
}
|
|
330
|
+
return new Set(['unknown']);
|
|
331
|
+
}
|
|
332
|
+
case 'BinaryExpression': {
|
|
333
|
+
if (ARITHMETIC.has(e.operator))
|
|
334
|
+
return new Set(['number']);
|
|
335
|
+
if (e.operator !== '+')
|
|
336
|
+
return new Set(['data']);
|
|
337
|
+
const l = kindOf(e.left, scope);
|
|
338
|
+
const r = kindOf(e.right, scope);
|
|
339
|
+
if (l.has('string') || r.has('string'))
|
|
340
|
+
return new Set(['string']);
|
|
341
|
+
if ([...l, ...r].every(k => k === 'number'))
|
|
342
|
+
return new Set(['number']);
|
|
343
|
+
return new Set(['data']);
|
|
344
|
+
}
|
|
345
|
+
case 'UnaryExpression':
|
|
346
|
+
return new Set([e.operator === '-' || e.operator === '+' || e.operator === '~' ? 'number' : e.operator === 'typeof' ? 'string' : 'data']);
|
|
347
|
+
case 'UpdateExpression':
|
|
348
|
+
return new Set(['number']);
|
|
349
|
+
case 'LogicalExpression':
|
|
350
|
+
return new Set([...kindOf(e.left, scope), ...kindOf(e.right, scope)]);
|
|
351
|
+
case 'ConditionalExpression':
|
|
352
|
+
return new Set([...kindOf(e.consequent, scope), ...kindOf(e.alternate, scope)]);
|
|
353
|
+
case 'AssignmentExpression':
|
|
354
|
+
return e.operator === '=' ? kindOf(e.right, scope) : new Set([e.operator === '+=' ? 'data' : 'number']);
|
|
355
|
+
case 'SequenceExpression':
|
|
356
|
+
return kindOf(e.expressions[e.expressions.length - 1], scope);
|
|
357
|
+
case 'ParenthesizedExpression':
|
|
358
|
+
return kindOf(e.expression, scope);
|
|
359
|
+
default:
|
|
360
|
+
return new Set(['unknown']);
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
const all = [];
|
|
364
|
+
const gather = (s) => all.push(...s.bindings.values());
|
|
365
|
+
gather(top);
|
|
366
|
+
for (const s of scopeOf.values())
|
|
367
|
+
gather(s);
|
|
368
|
+
for (let round = 0; round < 10; round++) {
|
|
369
|
+
let changed = false;
|
|
370
|
+
for (const b of all)
|
|
371
|
+
for (const src of b.sources)
|
|
372
|
+
for (const k of kindOf(src.expr, src.scope)) {
|
|
373
|
+
if (!b.kinds.has(k)) {
|
|
374
|
+
b.kinds.add(k);
|
|
375
|
+
changed = true;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (!changed)
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
for (const b of all)
|
|
382
|
+
if (b.kinds.size === 0)
|
|
383
|
+
b.kinds.add('unknown');
|
|
384
|
+
// 4. The rules.
|
|
385
|
+
const isPlainLocal = (e, scope) => {
|
|
386
|
+
if (e.type !== 'Identifier')
|
|
387
|
+
return false;
|
|
388
|
+
const b = scope.lookup(e.name);
|
|
389
|
+
return b !== undefined && !b.param && [...b.kinds].every(k => PLAIN.has(k));
|
|
390
|
+
};
|
|
391
|
+
const checkName = (name, objKinds, api, n, write) => {
|
|
392
|
+
for (const k of objKinds) {
|
|
393
|
+
const t = HOST_TABLES.get(k);
|
|
394
|
+
if (t) {
|
|
395
|
+
const m = t.get(name);
|
|
396
|
+
if (!m && k === 'doc' && (top.bindings.has(name) || known.has(name)))
|
|
397
|
+
continue;
|
|
398
|
+
// An XFA form's script objects are often named util or color, so another name on them is the form's own.
|
|
399
|
+
if (!m && xfa && SHADOWED.has(k) && !api_1.MACHINERY.has(name)) {
|
|
400
|
+
const host = api_1.HOST_DENY.get(name) ?? api_1.NODE_DENY.get(name);
|
|
401
|
+
if (host)
|
|
402
|
+
reportMember(host, api, n);
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
// A field holds every Field member, so another name is the form's own data, unless a viewer object has it.
|
|
406
|
+
if (!m && k === 'field' && !api_1.MACHINERY.has(name)) {
|
|
407
|
+
const host = api_1.HOST_DENY.get(name);
|
|
408
|
+
if (host)
|
|
409
|
+
reportMember(host, api, n);
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
if (!m)
|
|
413
|
+
report(types_1.ScriptCategory.Api, types_1.ScriptDetail.Unknown, n, { api });
|
|
414
|
+
else if ('finding' in m)
|
|
415
|
+
reportMember(m, api, n);
|
|
416
|
+
else if (write && !m.write)
|
|
417
|
+
report(types_1.ScriptCategory.Api, types_1.ScriptDetail.Unknown, n, { api: `${api} =` });
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
if (k === 'math' || k === 'builtin') {
|
|
421
|
+
const deny = api_1.BUILTIN_DENY.get(name);
|
|
422
|
+
if (deny)
|
|
423
|
+
reportMember(deny, api, n);
|
|
424
|
+
else if (api_1.MACHINERY.has(name))
|
|
425
|
+
report(types_1.ScriptCategory.DynamicCode, types_1.ScriptDetail.Prototype, n, { api });
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
if (k === 'constant' || k === 'fields')
|
|
429
|
+
continue;
|
|
430
|
+
if (api_1.MACHINERY.has(name))
|
|
431
|
+
report(types_1.ScriptCategory.DynamicCode, types_1.ScriptDetail.Prototype, n, { api });
|
|
432
|
+
const host = api_1.HOST_DENY.get(name) ?? (xfa || k === 'node' ? api_1.NODE_DENY.get(name) : undefined);
|
|
433
|
+
if (host)
|
|
434
|
+
reportMember(host, api, n);
|
|
435
|
+
// A node's href or prototype reference set at run time loads what it names.
|
|
436
|
+
else if (write && k === 'node' && api_1.URL_ATTRIBUTES.has(name))
|
|
437
|
+
report(types_1.ScriptCategory.External, types_1.ScriptDetail.Url, n, { api: `${api} =` });
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
const describe = (e) => {
|
|
441
|
+
if (e.type === 'Identifier')
|
|
442
|
+
return e.name;
|
|
443
|
+
if (e.type === 'ThisExpression')
|
|
444
|
+
return 'this';
|
|
445
|
+
if (e.type === 'MemberExpression') {
|
|
446
|
+
const name = memberName(e);
|
|
447
|
+
return `${describe(e.object)}${name === undefined ? '[...]' : e.computed ? `[${JSON.stringify(name)}]` : `.${name}`}`;
|
|
448
|
+
}
|
|
449
|
+
if (e.type === 'CallExpression')
|
|
450
|
+
return `${describe(e.callee)}()`;
|
|
451
|
+
return e.type;
|
|
452
|
+
};
|
|
453
|
+
const concatPieces = (e) => (e.type === 'BinaryExpression' && e.operator === '+' ? [...concatPieces(e.left), ...concatPieces(e.right)] : [e]);
|
|
454
|
+
const inConcat = new Set();
|
|
455
|
+
const inArithmetic = new Set();
|
|
456
|
+
const defined = [];
|
|
457
|
+
const visit = (n, scope, parent, write) => {
|
|
458
|
+
const inner = scopeOf.get(n) ?? scope;
|
|
459
|
+
switch (n.type) {
|
|
460
|
+
case 'WithStatement':
|
|
461
|
+
report(types_1.ScriptCategory.DynamicCode, types_1.ScriptDetail.With, n);
|
|
462
|
+
break;
|
|
463
|
+
case 'ImportExpression':
|
|
464
|
+
report(types_1.ScriptCategory.DynamicCode, types_1.ScriptDetail.Eval, n, { api: 'import()' });
|
|
465
|
+
break;
|
|
466
|
+
case 'TaggedTemplateExpression':
|
|
467
|
+
report(types_1.ScriptCategory.Api, types_1.ScriptDetail.Unknown, n, { api: `${describe(n.tag)}\`...\`` });
|
|
468
|
+
break;
|
|
469
|
+
case 'Identifier': {
|
|
470
|
+
if (!parent || !isReference(n, parent))
|
|
471
|
+
break;
|
|
472
|
+
// typeof on a name is a feature check: it reads nothing and never throws, whatever the name is.
|
|
473
|
+
if (parent.type === 'UnaryExpression' && parent.operator === 'typeof')
|
|
474
|
+
break;
|
|
475
|
+
const b = scope.lookup(n.name);
|
|
476
|
+
if (b)
|
|
477
|
+
break;
|
|
478
|
+
const denied = api_1.DENIED_GLOBALS.get(n.name);
|
|
479
|
+
if (denied) {
|
|
480
|
+
reportMember(denied, n.name, n);
|
|
481
|
+
break;
|
|
482
|
+
}
|
|
483
|
+
const som = xfa ? api_1.SOM_ROOTS.get(n.name) : undefined;
|
|
484
|
+
if (som !== undefined && typeof som !== 'string') {
|
|
485
|
+
reportMember(som, n.name, n);
|
|
486
|
+
break;
|
|
487
|
+
}
|
|
488
|
+
if (roots.has(n.name) || api_1.CONSTANT_ROOTS.has(n.name) || api_1.BUILTIN_NAMESPACES.has(n.name) || VALUES.has(n.name) || api_1.AFORM.has(n.name) || known.has(n.name) || api_1.BUILTIN_FUNCTIONS.has(n.name))
|
|
489
|
+
break;
|
|
490
|
+
if (xfa) {
|
|
491
|
+
// A form node by name, unless it is a name a viewer object has that leaves the form.
|
|
492
|
+
const host = api_1.HOST_DENY.get(n.name) ?? api_1.NODE_DENY.get(n.name);
|
|
493
|
+
if (host)
|
|
494
|
+
reportMember(host, n.name, n);
|
|
495
|
+
break;
|
|
496
|
+
}
|
|
497
|
+
const docMember = api_1.DOC.get(n.name);
|
|
498
|
+
if (docMember) {
|
|
499
|
+
reportMember(docMember, n.name, n);
|
|
500
|
+
if (write && 'kind' in docMember && !docMember.write)
|
|
501
|
+
report(types_1.ScriptCategory.Api, types_1.ScriptDetail.Unknown, n, { api: `${n.name} =` });
|
|
502
|
+
break;
|
|
503
|
+
}
|
|
504
|
+
report(types_1.ScriptCategory.Api, types_1.ScriptDetail.Unknown, n, { api: n.name });
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
case 'MemberExpression': {
|
|
508
|
+
const objKinds = kindOf(n.object, scope);
|
|
509
|
+
const api = describe(n);
|
|
510
|
+
const own = memberName(n);
|
|
511
|
+
if ((own === 'callee' || own === 'caller') && n.object.type === 'Identifier' && n.object.name === 'arguments' && !scope.lookup('arguments')) {
|
|
512
|
+
report(types_1.ScriptCategory.DynamicCode, types_1.ScriptDetail.SelfReference, n, { api });
|
|
513
|
+
break;
|
|
514
|
+
}
|
|
515
|
+
if (n.computed) {
|
|
516
|
+
const p = n.property;
|
|
517
|
+
if (p.type === 'Literal' && typeof p.value === 'string') {
|
|
518
|
+
// A form node is reached by quoted name when its name is not an identifier, so only its members are checked.
|
|
519
|
+
if ([...objKinds].some(k => api_1.HOST_KINDS.has(k) && k !== 'node'))
|
|
520
|
+
report(types_1.ScriptCategory.Obfuscation, types_1.ScriptDetail.BracketName, n, { api });
|
|
521
|
+
checkName(p.value, objKinds, api, n, write);
|
|
522
|
+
}
|
|
523
|
+
else if (p.type === 'Literal' && typeof p.value === 'number') {
|
|
524
|
+
if ([...objKinds].some(k => HOST_TABLES.has(k)))
|
|
525
|
+
report(types_1.ScriptCategory.Api, types_1.ScriptDetail.Unknown, n, { api });
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
const keyKinds = kindOf(p, scope);
|
|
529
|
+
const numericKey = [...keyKinds].every(k => k === 'number');
|
|
530
|
+
const onHost = [...objKinds].some(k => HOST_TABLES.has(k) || k === 'math' || k === 'builtin' || k === 'constant');
|
|
531
|
+
if (onHost || (!numericKey && !isPlainLocal(n.object, scope)))
|
|
532
|
+
report(types_1.ScriptCategory.DynamicCode, types_1.ScriptDetail.ComputedAccess, n, { api });
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
else if (n.property.type === 'Identifier')
|
|
536
|
+
checkName(n.property.name, objKinds, api, n, write);
|
|
537
|
+
break;
|
|
538
|
+
}
|
|
539
|
+
case 'CallExpression': {
|
|
540
|
+
const callee = describe(n.callee);
|
|
541
|
+
if (callee === 'util.printf')
|
|
542
|
+
checkPrintf(n.arguments[0], report, n);
|
|
543
|
+
if (callee === 'String.fromCharCode' || callee === 'String.fromCodePoint')
|
|
544
|
+
report(types_1.ScriptCategory.Obfuscation, types_1.ScriptDetail.CharCodes, n, { api: callee, count: n.arguments.length });
|
|
545
|
+
const method = n.callee.type === 'MemberExpression' ? memberName(n.callee) : undefined;
|
|
546
|
+
const receiver = n.callee.type === 'MemberExpression' ? n.callee.object : undefined;
|
|
547
|
+
if (method && receiver) {
|
|
548
|
+
const on = kindOf(receiver, scope);
|
|
549
|
+
const first = n.arguments[0];
|
|
550
|
+
const literal = first?.type === 'Literal' && typeof first.value === 'string' ? first.value : undefined;
|
|
551
|
+
// app.response(q, title, default, true), or with bPassword set: a password asked for in a prompt.
|
|
552
|
+
if ((on.has('app') || on.has('xfahost')) && method === 'response') {
|
|
553
|
+
const opts = first?.type === 'ObjectExpression' ? first : undefined;
|
|
554
|
+
const flag = opts
|
|
555
|
+
? opts.properties.find(p => p.type === 'Property' && !p.computed && ((p.key.type === 'Identifier' && p.key.name === 'bPassword') || (p.key.type === 'Literal' && p.key.value === 'bPassword')))
|
|
556
|
+
: n.arguments[3];
|
|
557
|
+
const value = flag?.type === 'Property' ? flag.value : flag;
|
|
558
|
+
const off = !value || (value.type === 'Literal' && (value.value === false || value.value === 0 || value.value === null));
|
|
559
|
+
if (!off)
|
|
560
|
+
report(types_1.ScriptCategory.Api, types_1.ScriptDetail.PasswordPrompt, n, { api: callee });
|
|
561
|
+
}
|
|
562
|
+
if (on.has('xmldata') && method === 'parse') {
|
|
563
|
+
const allow = n.arguments[1];
|
|
564
|
+
if (!(allow?.type === 'Literal' && allow.value === false))
|
|
565
|
+
report(types_1.ScriptCategory.External, types_1.ScriptDetail.ImportExport, n, { api: 'XMLData.parse with entities' });
|
|
566
|
+
}
|
|
567
|
+
const nodeLike = on.has('node') || on.has('xfa') || on.has('unknown');
|
|
568
|
+
if (nodeLike && method === 'createNode' && (literal === undefined || api_1.CODE_NODES.has(literal)))
|
|
569
|
+
report(types_1.ScriptCategory.DynamicCode, types_1.ScriptDetail.InstallScript, n, { api: `${callee}(${literal === undefined ? '...' : JSON.stringify(literal)})` });
|
|
570
|
+
if (nodeLike && DATA_ONLY.has(method) && !DATA_PATH.test(describe(receiver)))
|
|
571
|
+
report(types_1.ScriptCategory.DynamicCode, types_1.ScriptDetail.InstallScript, n, { api: callee });
|
|
572
|
+
if (nodeLike && method === 'setAttribute') {
|
|
573
|
+
const name = n.arguments[1];
|
|
574
|
+
const attr = name?.type === 'Literal' && typeof name.value === 'string' ? name.value : undefined;
|
|
575
|
+
if (attr === undefined || api_1.URL_ATTRIBUTES.has(attr))
|
|
576
|
+
report(types_1.ScriptCategory.External, types_1.ScriptDetail.Url, n, { api: `${callee}(..., ${attr === undefined ? '...' : JSON.stringify(attr)})` });
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
// (1525081).toString(36) spells "word": text written as a number in a base past 10.
|
|
580
|
+
if (method === 'toString' && receiver) {
|
|
581
|
+
const radix = n.arguments[0];
|
|
582
|
+
const big = radix?.type === 'Literal' && typeof radix.value === 'number' && (radix.value > 16 || (radix.value > 10 && receiver.type === 'Literal'));
|
|
583
|
+
if (big || (radix && radix.type !== 'Literal' && receiver.type === 'Literal'))
|
|
584
|
+
report(types_1.ScriptCategory.Obfuscation, types_1.ScriptDetail.Decode, n, { api: `${describe(receiver)}.toString(radix)` });
|
|
585
|
+
}
|
|
586
|
+
// "qwqoqrqd".replace(/q/g, ""): text hidden among filler in a string the script wrote itself.
|
|
587
|
+
if ((method === 'replace' || method === 'replaceAll' || method === 'split') && receiver?.type === 'Literal' && typeof receiver.value === 'string' && receiver.value.length > 3) {
|
|
588
|
+
report(types_1.ScriptCategory.Obfuscation, types_1.ScriptDetail.Decode, n, { api: `"...".${method}()` });
|
|
589
|
+
}
|
|
590
|
+
if (n.callee.type === 'MemberExpression' && memberName(n.callee) === 'repeat') {
|
|
591
|
+
const count = n.arguments[0];
|
|
592
|
+
if (count?.type === 'Literal' && typeof count.value === 'number' && count.value > 1000)
|
|
593
|
+
report(types_1.ScriptCategory.Obfuscation, types_1.ScriptDetail.LongString, n, { length: count.value });
|
|
594
|
+
}
|
|
595
|
+
if (n.callee.type === 'MemberExpression' && memberName(n.callee) === 'toString' && n.callee.object.type !== 'Literal') {
|
|
596
|
+
const k = kindOf(n.callee.object, scope);
|
|
597
|
+
const target = n.callee.object;
|
|
598
|
+
const isFn = target.type === 'Identifier' && isFunctionBinding(target.name, scope);
|
|
599
|
+
if (isFn || target.type === 'FunctionExpression' || target.type === 'ArrowFunctionExpression' || (k.has('data') && target.type === 'MemberExpression' && memberName(target) === 'callee')) {
|
|
600
|
+
report(types_1.ScriptCategory.DynamicCode, types_1.ScriptDetail.SelfReference, n, { api: `${describe(target)}.toString()` });
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
break;
|
|
604
|
+
}
|
|
605
|
+
case 'Literal': {
|
|
606
|
+
if (typeof n.value !== 'string')
|
|
607
|
+
break;
|
|
608
|
+
const s = n.value;
|
|
609
|
+
if (exports.ENCODED.some(re => re.test(s)) || exports.ENCODED.some(re => re.test(n.raw ?? '')))
|
|
610
|
+
report(types_1.ScriptCategory.Obfuscation, types_1.ScriptDetail.EncodedString, n, { length: s.length });
|
|
611
|
+
else if (isLongBlob(s))
|
|
612
|
+
report(types_1.ScriptCategory.Obfuscation, types_1.ScriptDetail.LongString, n, { length: s.length });
|
|
613
|
+
const escapes = (n.raw ?? '').match(NEEDLESS_ESCAPE)?.length ?? 0;
|
|
614
|
+
if (escapes >= 3)
|
|
615
|
+
report(types_1.ScriptCategory.Obfuscation, types_1.ScriptDetail.Escapes, n, { count: escapes });
|
|
616
|
+
break;
|
|
617
|
+
}
|
|
618
|
+
case 'TemplateElement': {
|
|
619
|
+
const s = n.value.cooked ?? n.value.raw;
|
|
620
|
+
if (exports.ENCODED.some(re => re.test(s)))
|
|
621
|
+
report(types_1.ScriptCategory.Obfuscation, types_1.ScriptDetail.EncodedString, n, { length: s.length });
|
|
622
|
+
else if (isLongBlob(s))
|
|
623
|
+
report(types_1.ScriptCategory.Obfuscation, types_1.ScriptDetail.LongString, n, { length: s.length });
|
|
624
|
+
break;
|
|
625
|
+
}
|
|
626
|
+
case 'BinaryExpression': {
|
|
627
|
+
if ((n.operator === '+' || n.operator === '-' || n.operator === '*') && !inArithmetic.has(n)) {
|
|
628
|
+
// 0x1a2+0x3b4+-0x556 is 0: a number hidden as arithmetic on hex constants.
|
|
629
|
+
const leaves = [];
|
|
630
|
+
const walk = (x) => {
|
|
631
|
+
if (x.type === 'BinaryExpression' && (x.operator === '+' || x.operator === '-' || x.operator === '*')) {
|
|
632
|
+
inArithmetic.add(x);
|
|
633
|
+
walk(x.left);
|
|
634
|
+
walk(x.right);
|
|
635
|
+
}
|
|
636
|
+
else
|
|
637
|
+
leaves.push(x.type === 'UnaryExpression' && x.operator === '-' ? x.argument : x);
|
|
638
|
+
};
|
|
639
|
+
walk(n);
|
|
640
|
+
const numbers = leaves.filter(l => l.type === 'Literal' && typeof l.value === 'number');
|
|
641
|
+
if (numbers.length === leaves.length && numbers.length >= 3 && numbers.some(l => l.type === 'Literal' && /^0x/i.test(l.raw ?? '')))
|
|
642
|
+
report(types_1.ScriptCategory.Obfuscation, types_1.ScriptDetail.NumberExpression, n, { count: numbers.length });
|
|
643
|
+
}
|
|
644
|
+
if (n.operator !== '+' || inConcat.has(n))
|
|
645
|
+
break;
|
|
646
|
+
const pieces = concatPieces(n);
|
|
647
|
+
// The nested + nodes of this chain are counted here, not again on their own.
|
|
648
|
+
const mark = (x) => {
|
|
649
|
+
if (x.type === 'BinaryExpression' && x.operator === '+') {
|
|
650
|
+
inConcat.add(x);
|
|
651
|
+
mark(x.left);
|
|
652
|
+
mark(x.right);
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
mark(n);
|
|
656
|
+
const tiny = pieces.filter(p => (p.type === 'Literal' && typeof p.value === 'string' && p.value.length <= 2) || (p.type === 'CallExpression' && /fromCharCode$/.test(describe(p.callee)))).length;
|
|
657
|
+
const literals = pieces.filter(p => p.type === 'Literal' && typeof p.value === 'string').length;
|
|
658
|
+
if (tiny >= 6 || literals >= 25)
|
|
659
|
+
report(types_1.ScriptCategory.Obfuscation, types_1.ScriptDetail.Concatenation, n, { count: pieces.length });
|
|
660
|
+
break;
|
|
661
|
+
}
|
|
662
|
+
default:
|
|
663
|
+
break;
|
|
664
|
+
}
|
|
665
|
+
for (const c of children(n)) {
|
|
666
|
+
const isWriteTarget = (n.type === 'AssignmentExpression' && c === n.left) || (n.type === 'UpdateExpression' && c === n.argument);
|
|
667
|
+
visit(c, inner, n, isWriteTarget);
|
|
668
|
+
}
|
|
669
|
+
};
|
|
670
|
+
visit(program, top, undefined, false);
|
|
671
|
+
// Every name the top scope holds is global: declared there, assigned without a declaration, or set on `this`.
|
|
672
|
+
// An XFA script's names stay in its own script; other scripts reach them through the node that holds it.
|
|
673
|
+
if (!xfa)
|
|
674
|
+
for (const name of top.bindings.keys())
|
|
675
|
+
defined.push(name);
|
|
676
|
+
if (!findings.some(f => f.action === 'strip'))
|
|
677
|
+
report(types_1.ScriptCategory.Form, types_1.ScriptDetail.Computation);
|
|
678
|
+
return { findings, definedGlobals: [...new Set(defined)] };
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* A string over 64KB, or over 1000 characters with a stretch of 64 or more that has no whitespace: a payload or an
|
|
682
|
+
* encoded blob. Text and tables of numbers break into words; the forms sampled never ran past 18.
|
|
683
|
+
*/
|
|
684
|
+
function isLongBlob(s) {
|
|
685
|
+
if (s.length <= 1000)
|
|
686
|
+
return false;
|
|
687
|
+
if (s.length > 65536)
|
|
688
|
+
return true;
|
|
689
|
+
let run = 0;
|
|
690
|
+
for (let i = 0; i < s.length; i++) {
|
|
691
|
+
const c = s.charCodeAt(i);
|
|
692
|
+
run = c === 32 || c === 9 || c === 10 || c === 13 ? 0 : run + 1;
|
|
693
|
+
if (run >= 64)
|
|
694
|
+
return true;
|
|
695
|
+
}
|
|
696
|
+
return false;
|
|
697
|
+
}
|
|
698
|
+
function memberName(e) {
|
|
699
|
+
if (!e.computed && e.property.type === 'Identifier')
|
|
700
|
+
return e.property.name;
|
|
701
|
+
if (e.computed && e.property.type === 'Literal' && (typeof e.property.value === 'string' || typeof e.property.value === 'number'))
|
|
702
|
+
return String(e.property.value);
|
|
703
|
+
return undefined;
|
|
704
|
+
}
|
|
705
|
+
/** Whether an identifier is read or written as a variable, not a property name, label or declaration. */
|
|
706
|
+
function isReference(n, parent) {
|
|
707
|
+
switch (parent.type) {
|
|
708
|
+
case 'MemberExpression':
|
|
709
|
+
return parent.object === n || (parent.computed && parent.property === n);
|
|
710
|
+
case 'Property':
|
|
711
|
+
return parent.value === n || (parent.computed && parent.key === n);
|
|
712
|
+
case 'PropertyDefinition':
|
|
713
|
+
case 'MethodDefinition':
|
|
714
|
+
return parent.computed && parent.key === n;
|
|
715
|
+
case 'VariableDeclarator':
|
|
716
|
+
return parent.init === n;
|
|
717
|
+
case 'FunctionDeclaration':
|
|
718
|
+
case 'FunctionExpression':
|
|
719
|
+
case 'ArrowFunctionExpression':
|
|
720
|
+
return false;
|
|
721
|
+
case 'LabeledStatement':
|
|
722
|
+
case 'BreakStatement':
|
|
723
|
+
case 'ContinueStatement':
|
|
724
|
+
return false;
|
|
725
|
+
case 'CatchClause':
|
|
726
|
+
return false;
|
|
727
|
+
case 'ClassDeclaration':
|
|
728
|
+
case 'ClassExpression':
|
|
729
|
+
return parent.superClass === n;
|
|
730
|
+
case 'AssignmentPattern':
|
|
731
|
+
return parent.right === n;
|
|
732
|
+
case 'ArrayPattern':
|
|
733
|
+
case 'ObjectPattern':
|
|
734
|
+
case 'RestElement':
|
|
735
|
+
return false;
|
|
736
|
+
case 'ExportSpecifier':
|
|
737
|
+
case 'ImportSpecifier':
|
|
738
|
+
case 'ImportDefaultSpecifier':
|
|
739
|
+
case 'ImportNamespaceSpecifier':
|
|
740
|
+
return false;
|
|
741
|
+
case 'MetaProperty':
|
|
742
|
+
return false;
|
|
743
|
+
default:
|
|
744
|
+
return true;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
function isFunctionBinding(name, scope) {
|
|
748
|
+
const b = scope.lookup(name);
|
|
749
|
+
return b !== undefined && !b.param && (b.fn === true || b.sources.some(s => s.expr.type === 'FunctionExpression' || s.expr.type === 'ArrowFunctionExpression'));
|
|
750
|
+
}
|
|
751
|
+
/** util.printf with a format a script did not write out, or with a field width past 100: the CVE-2008-2992 overflow. */
|
|
752
|
+
function checkPrintf(format, report, n) {
|
|
753
|
+
if (format?.type !== 'Literal' || typeof format.value !== 'string') {
|
|
754
|
+
report(types_1.ScriptCategory.Api, types_1.ScriptDetail.Exploited, n, { api: 'util.printf with a computed format' });
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
for (const m of format.value.matchAll(/%[-+ #0,]*(\d+)?(?:\.(\d+))?[a-zA-Z]/g)) {
|
|
758
|
+
if (Number(m[1] ?? 0) > 100 || Number(m[2] ?? 0) > 100) {
|
|
759
|
+
report(types_1.ScriptCategory.Api, types_1.ScriptDetail.Exploited, n, { api: 'util.printf with a field width past 100' });
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
function lineOf(text, offset) {
|
|
765
|
+
let line = 1;
|
|
766
|
+
for (let i = 0; i < offset && i < text.length; i++)
|
|
767
|
+
if (text.charCodeAt(i) === 10)
|
|
768
|
+
line++;
|
|
769
|
+
return line;
|
|
770
|
+
}
|