@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/formcalc.js
ADDED
|
@@ -0,0 +1,792 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.analyzeFormCalc = analyzeFormCalc;
|
|
4
|
+
const analyze_1 = require("./analyze");
|
|
5
|
+
const api_1 = require("./api");
|
|
6
|
+
const findings_1 = require("./findings");
|
|
7
|
+
const types_1 = require("./types");
|
|
8
|
+
const KEYWORDS = new Set([
|
|
9
|
+
'and',
|
|
10
|
+
'break',
|
|
11
|
+
'continue',
|
|
12
|
+
'do',
|
|
13
|
+
'downto',
|
|
14
|
+
'else',
|
|
15
|
+
'elseif',
|
|
16
|
+
'end',
|
|
17
|
+
'endfor',
|
|
18
|
+
'endfunc',
|
|
19
|
+
'endif',
|
|
20
|
+
'endwhile',
|
|
21
|
+
'eq',
|
|
22
|
+
'for',
|
|
23
|
+
'foreach',
|
|
24
|
+
'func',
|
|
25
|
+
'ge',
|
|
26
|
+
'gt',
|
|
27
|
+
'if',
|
|
28
|
+
'in',
|
|
29
|
+
'infinity',
|
|
30
|
+
'le',
|
|
31
|
+
'lt',
|
|
32
|
+
'nan',
|
|
33
|
+
'ne',
|
|
34
|
+
'not',
|
|
35
|
+
'null',
|
|
36
|
+
'or',
|
|
37
|
+
'return',
|
|
38
|
+
'step',
|
|
39
|
+
'then',
|
|
40
|
+
'this',
|
|
41
|
+
'throw',
|
|
42
|
+
'upto',
|
|
43
|
+
'var',
|
|
44
|
+
'while',
|
|
45
|
+
]);
|
|
46
|
+
/** The built-in functions, by lower-case name. FormCalc names are case-insensitive. */
|
|
47
|
+
const FUNCTIONS = new Set([
|
|
48
|
+
'Abs',
|
|
49
|
+
'Avg',
|
|
50
|
+
'Ceil',
|
|
51
|
+
'Count',
|
|
52
|
+
'Floor',
|
|
53
|
+
'Max',
|
|
54
|
+
'Min',
|
|
55
|
+
'Mod',
|
|
56
|
+
'Round',
|
|
57
|
+
'Sum',
|
|
58
|
+
'Date',
|
|
59
|
+
'Date2Num',
|
|
60
|
+
'DateFmt',
|
|
61
|
+
'IsoDate2Num',
|
|
62
|
+
'IsoTime2Num',
|
|
63
|
+
'LocalDateFmt',
|
|
64
|
+
'LocalTimeFmt',
|
|
65
|
+
'Num2Date',
|
|
66
|
+
'Num2GMTime',
|
|
67
|
+
'Num2Time',
|
|
68
|
+
'Time',
|
|
69
|
+
'Time2Num',
|
|
70
|
+
'TimeFmt',
|
|
71
|
+
'Apr',
|
|
72
|
+
'CTerm',
|
|
73
|
+
'FV',
|
|
74
|
+
'IPmt',
|
|
75
|
+
'NPV',
|
|
76
|
+
'Pmt',
|
|
77
|
+
'PPmt',
|
|
78
|
+
'PV',
|
|
79
|
+
'Rate',
|
|
80
|
+
'Term',
|
|
81
|
+
'Choose',
|
|
82
|
+
'Exists',
|
|
83
|
+
'HasValue',
|
|
84
|
+
'Oneof',
|
|
85
|
+
'Within',
|
|
86
|
+
'Null',
|
|
87
|
+
'Ref',
|
|
88
|
+
'UnitType',
|
|
89
|
+
'UnitValue',
|
|
90
|
+
'At',
|
|
91
|
+
'Concat',
|
|
92
|
+
'Encode',
|
|
93
|
+
'Format',
|
|
94
|
+
'Left',
|
|
95
|
+
'Len',
|
|
96
|
+
'Lower',
|
|
97
|
+
'Ltrim',
|
|
98
|
+
'Parse',
|
|
99
|
+
'Replace',
|
|
100
|
+
'Right',
|
|
101
|
+
'Rtrim',
|
|
102
|
+
'Space',
|
|
103
|
+
'Str',
|
|
104
|
+
'Stuff',
|
|
105
|
+
'Substr',
|
|
106
|
+
'Uuid',
|
|
107
|
+
'Upper',
|
|
108
|
+
'WordNum',
|
|
109
|
+
].map(s => s.toLowerCase()));
|
|
110
|
+
/** Built-in functions that reach outside the form, run text as code, or uncover hidden text. */
|
|
111
|
+
const DENIED_FUNCTIONS = new Map([
|
|
112
|
+
['get', [types_1.ScriptCategory.External, types_1.ScriptDetail.Network]],
|
|
113
|
+
['post', [types_1.ScriptCategory.External, types_1.ScriptDetail.Network]],
|
|
114
|
+
['put', [types_1.ScriptCategory.External, types_1.ScriptDetail.Network]],
|
|
115
|
+
['eval', [types_1.ScriptCategory.DynamicCode, types_1.ScriptDetail.Eval]],
|
|
116
|
+
['decode', [types_1.ScriptCategory.Obfuscation, types_1.ScriptDetail.Decode]],
|
|
117
|
+
]);
|
|
118
|
+
const TABLES = new Map([
|
|
119
|
+
['xfa', api_1.XFA_ROOT],
|
|
120
|
+
['xfahost', api_1.XFA_HOST],
|
|
121
|
+
['xfalayout', api_1.XFA_LAYOUT],
|
|
122
|
+
['xfaevent', api_1.XFA_EVENT],
|
|
123
|
+
['doc', api_1.DOC],
|
|
124
|
+
]);
|
|
125
|
+
/** Node methods that change the form's own trees, which only data may go through. */
|
|
126
|
+
const DATA_ONLY = new Set(['loadXML', 'assignNode']);
|
|
127
|
+
const DATA_PATH = /^(?:xfa\.(?:datasets|data|record)|\$data|\$record|!)/;
|
|
128
|
+
/** \u00XX escapes of printable ASCII, which a script has no reason to write. */
|
|
129
|
+
const NEEDLESS_ESCAPE = /\\u00(?:[2-7][0-9a-fA-F])/g;
|
|
130
|
+
class FormCalcSyntaxError extends Error {
|
|
131
|
+
}
|
|
132
|
+
/** Space, tab, CR, LF, form feed, vertical tab, no-break space and the byte order mark. */
|
|
133
|
+
const WHITESPACE = new Set([0x20, 0x09, 0x0d, 0x0a, 0x0c, 0x0b, 0xa0, 0xfeff]);
|
|
134
|
+
function tokenize(text) {
|
|
135
|
+
const out = [];
|
|
136
|
+
let i = 0;
|
|
137
|
+
const n = text.length;
|
|
138
|
+
const isIdStart = (c) => /[A-Za-z_$!#]/.test(c) || c.charCodeAt(0) >= 0x80;
|
|
139
|
+
const isId = (c) => /[A-Za-z0-9_$]/.test(c) || c.charCodeAt(0) >= 0x80;
|
|
140
|
+
const isDigit = (c) => c !== undefined && c >= '0' && c <= '9';
|
|
141
|
+
while (i < n) {
|
|
142
|
+
const c = text[i];
|
|
143
|
+
if (WHITESPACE.has(c.charCodeAt(0))) {
|
|
144
|
+
i++;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (c === ';' || (c === '/' && text[i + 1] === '/')) {
|
|
148
|
+
while (i < n && text[i] !== '\n' && text[i] !== '\r')
|
|
149
|
+
i++;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const pos = i;
|
|
153
|
+
if (c === '"') {
|
|
154
|
+
let value = '';
|
|
155
|
+
i++;
|
|
156
|
+
for (;;) {
|
|
157
|
+
if (i >= n)
|
|
158
|
+
throw new FormCalcSyntaxError(`unterminated string at ${pos}`);
|
|
159
|
+
const d = text[i];
|
|
160
|
+
if (d === '"') {
|
|
161
|
+
if (text[i + 1] === '"') {
|
|
162
|
+
value += '"';
|
|
163
|
+
i += 2;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
i++;
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
if (d === '\\' && (text[i + 1] === 'u' || text[i + 1] === 'U') && /^[0-9a-fA-F]{4}$/.test(text.slice(i + 2, i + 6))) {
|
|
170
|
+
value += String.fromCharCode(Number.parseInt(text.slice(i + 2, i + 6), 16));
|
|
171
|
+
i += 6;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
value += d;
|
|
175
|
+
i++;
|
|
176
|
+
}
|
|
177
|
+
out.push({ t: 'str', v: value, pos, raw: text.slice(pos, i) });
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const prev = out[out.length - 1];
|
|
181
|
+
const operand = !prev || (prev.t === 'op' && prev.v !== ')' && prev.v !== ']') || (prev.t === 'kw' && !['null', 'infinity', 'nan', 'this'].includes(prev.v));
|
|
182
|
+
if (isDigit(c) || (c === '.' && isDigit(text[i + 1]) && operand)) {
|
|
183
|
+
const m = /^(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?/.exec(text.slice(i, i + 400));
|
|
184
|
+
if (!m)
|
|
185
|
+
throw new FormCalcSyntaxError(`bad number at ${pos}`);
|
|
186
|
+
out.push({ t: 'num', v: m[0], pos });
|
|
187
|
+
i += m[0].length;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (isIdStart(c)) {
|
|
191
|
+
i++;
|
|
192
|
+
while (i < n && isId(text[i]))
|
|
193
|
+
i++;
|
|
194
|
+
const v = text.slice(pos, i);
|
|
195
|
+
const lower = v.toLowerCase();
|
|
196
|
+
if (KEYWORDS.has(lower))
|
|
197
|
+
out.push({ t: 'kw', v: lower, pos });
|
|
198
|
+
else
|
|
199
|
+
out.push({ t: 'id', v, pos });
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const two = text.slice(i, i + 2);
|
|
203
|
+
if (two === '==' || two === '<>' || two === '<=' || two === '>=' || two === '..') {
|
|
204
|
+
out.push({ t: 'op', v: two, pos });
|
|
205
|
+
i += 2;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if ('=<>+-*/&|()[],.'.includes(c)) {
|
|
209
|
+
out.push({ t: 'op', v: c, pos });
|
|
210
|
+
i++;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
throw new FormCalcSyntaxError(`unexpected ${JSON.stringify(c)} at ${pos}`);
|
|
214
|
+
}
|
|
215
|
+
out.push({ t: 'eof', v: '', pos: n });
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
218
|
+
class Parser {
|
|
219
|
+
toks;
|
|
220
|
+
maxDepth;
|
|
221
|
+
i = 0;
|
|
222
|
+
depth = 0;
|
|
223
|
+
deepest = 0;
|
|
224
|
+
constructor(toks, maxDepth) {
|
|
225
|
+
this.toks = toks;
|
|
226
|
+
this.maxDepth = maxDepth;
|
|
227
|
+
}
|
|
228
|
+
get tok() {
|
|
229
|
+
return this.toks[this.i];
|
|
230
|
+
}
|
|
231
|
+
next() {
|
|
232
|
+
return this.toks[this.i++];
|
|
233
|
+
}
|
|
234
|
+
isOp(v) {
|
|
235
|
+
return this.tok.t === 'op' && this.tok.v === v;
|
|
236
|
+
}
|
|
237
|
+
isKw(...v) {
|
|
238
|
+
return this.tok.t === 'kw' && v.includes(this.tok.v);
|
|
239
|
+
}
|
|
240
|
+
expectOp(v) {
|
|
241
|
+
if (!this.isOp(v))
|
|
242
|
+
this.fail(`expected ${v}`);
|
|
243
|
+
this.i++;
|
|
244
|
+
}
|
|
245
|
+
expectKw(v) {
|
|
246
|
+
if (!this.isKw(v))
|
|
247
|
+
this.fail(`expected ${v}`);
|
|
248
|
+
this.i++;
|
|
249
|
+
}
|
|
250
|
+
fail(msg) {
|
|
251
|
+
throw new FormCalcSyntaxError(`${msg} at ${this.tok.pos}${this.tok.v ? ` (${JSON.stringify(this.tok.v)})` : ''}`);
|
|
252
|
+
}
|
|
253
|
+
enter() {
|
|
254
|
+
this.depth++;
|
|
255
|
+
if (this.depth > this.deepest)
|
|
256
|
+
this.deepest = this.depth;
|
|
257
|
+
if (this.depth > this.maxDepth)
|
|
258
|
+
throw new DepthError();
|
|
259
|
+
}
|
|
260
|
+
leave() {
|
|
261
|
+
this.depth--;
|
|
262
|
+
}
|
|
263
|
+
program() {
|
|
264
|
+
const list = this.list([]);
|
|
265
|
+
if (this.tok.t !== 'eof')
|
|
266
|
+
this.fail('unexpected');
|
|
267
|
+
return list;
|
|
268
|
+
}
|
|
269
|
+
/** Expressions up to one of the keywords that close the construct, which is left for the caller. */
|
|
270
|
+
list(stop) {
|
|
271
|
+
const out = [];
|
|
272
|
+
while (this.tok.t !== 'eof' && !this.isKw(...stop))
|
|
273
|
+
out.push(this.expression());
|
|
274
|
+
if (stop.length && this.tok.t === 'eof')
|
|
275
|
+
this.fail(`expected ${stop.join(' or ')}`);
|
|
276
|
+
return out;
|
|
277
|
+
}
|
|
278
|
+
ident() {
|
|
279
|
+
if (this.tok.t !== 'id')
|
|
280
|
+
this.fail('expected a name');
|
|
281
|
+
return this.next().v;
|
|
282
|
+
}
|
|
283
|
+
expression() {
|
|
284
|
+
this.enter();
|
|
285
|
+
try {
|
|
286
|
+
const t = this.tok;
|
|
287
|
+
if (t.t === 'kw') {
|
|
288
|
+
switch (t.v) {
|
|
289
|
+
case 'if': {
|
|
290
|
+
this.i++;
|
|
291
|
+
const parts = [];
|
|
292
|
+
this.expectOp('(');
|
|
293
|
+
parts.push(this.simple());
|
|
294
|
+
this.expectOp(')');
|
|
295
|
+
this.expectKw('then');
|
|
296
|
+
parts.push(...this.list(['elseif', 'else', 'endif']));
|
|
297
|
+
while (this.isKw('elseif')) {
|
|
298
|
+
this.i++;
|
|
299
|
+
this.expectOp('(');
|
|
300
|
+
parts.push(this.simple());
|
|
301
|
+
this.expectOp(')');
|
|
302
|
+
this.expectKw('then');
|
|
303
|
+
parts.push(...this.list(['elseif', 'else', 'endif']));
|
|
304
|
+
}
|
|
305
|
+
if (this.isKw('else')) {
|
|
306
|
+
this.i++;
|
|
307
|
+
parts.push(...this.list(['endif']));
|
|
308
|
+
}
|
|
309
|
+
this.expectKw('endif');
|
|
310
|
+
return { t: 'block', parts, pos: t.pos };
|
|
311
|
+
}
|
|
312
|
+
case 'while': {
|
|
313
|
+
this.i++;
|
|
314
|
+
this.expectOp('(');
|
|
315
|
+
const parts = [this.simple()];
|
|
316
|
+
this.expectOp(')');
|
|
317
|
+
this.expectKw('do');
|
|
318
|
+
parts.push(...this.list(['endwhile']));
|
|
319
|
+
this.expectKw('endwhile');
|
|
320
|
+
return { t: 'block', parts, pos: t.pos };
|
|
321
|
+
}
|
|
322
|
+
case 'for': {
|
|
323
|
+
this.i++;
|
|
324
|
+
if (this.isKw('var'))
|
|
325
|
+
this.i++;
|
|
326
|
+
const name = this.ident();
|
|
327
|
+
this.expectOp('=');
|
|
328
|
+
const parts = [{ t: 'var', name, init: this.simple(), pos: t.pos }];
|
|
329
|
+
if (!this.isKw('upto', 'downto'))
|
|
330
|
+
this.fail('expected upto or downto');
|
|
331
|
+
this.i++;
|
|
332
|
+
parts.push(this.simple());
|
|
333
|
+
if (this.isKw('step')) {
|
|
334
|
+
this.i++;
|
|
335
|
+
parts.push(this.simple());
|
|
336
|
+
}
|
|
337
|
+
this.expectKw('do');
|
|
338
|
+
parts.push(...this.list(['endfor']));
|
|
339
|
+
this.expectKw('endfor');
|
|
340
|
+
return { t: 'block', parts, pos: t.pos };
|
|
341
|
+
}
|
|
342
|
+
case 'foreach': {
|
|
343
|
+
this.i++;
|
|
344
|
+
const name = this.ident();
|
|
345
|
+
this.expectKw('in');
|
|
346
|
+
this.expectOp('(');
|
|
347
|
+
const parts = [{ t: 'var', name, pos: t.pos }, ...this.args()];
|
|
348
|
+
this.expectKw('do');
|
|
349
|
+
parts.push(...this.list(['endfor']));
|
|
350
|
+
this.expectKw('endfor');
|
|
351
|
+
return { t: 'block', parts, pos: t.pos };
|
|
352
|
+
}
|
|
353
|
+
case 'do': {
|
|
354
|
+
this.i++;
|
|
355
|
+
const parts = this.list(['end']);
|
|
356
|
+
this.expectKw('end');
|
|
357
|
+
return { t: 'block', parts, pos: t.pos };
|
|
358
|
+
}
|
|
359
|
+
case 'var': {
|
|
360
|
+
this.i++;
|
|
361
|
+
const name = this.ident();
|
|
362
|
+
if (this.isOp('=')) {
|
|
363
|
+
this.i++;
|
|
364
|
+
return { t: 'var', name, init: this.simple(), pos: t.pos };
|
|
365
|
+
}
|
|
366
|
+
return { t: 'var', name, pos: t.pos };
|
|
367
|
+
}
|
|
368
|
+
case 'func': {
|
|
369
|
+
this.i++;
|
|
370
|
+
const name = this.ident();
|
|
371
|
+
this.expectOp('(');
|
|
372
|
+
const params = [];
|
|
373
|
+
if (!this.isOp(')')) {
|
|
374
|
+
params.push(this.ident());
|
|
375
|
+
while (this.isOp(',')) {
|
|
376
|
+
this.i++;
|
|
377
|
+
params.push(this.ident());
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
this.expectOp(')');
|
|
381
|
+
this.expectKw('do');
|
|
382
|
+
const body = this.list(['endfunc']);
|
|
383
|
+
this.expectKw('endfunc');
|
|
384
|
+
return { t: 'func', name, params, body, pos: t.pos };
|
|
385
|
+
}
|
|
386
|
+
case 'break':
|
|
387
|
+
case 'continue':
|
|
388
|
+
this.i++;
|
|
389
|
+
return { t: 'lit', pos: t.pos };
|
|
390
|
+
case 'return':
|
|
391
|
+
case 'throw': {
|
|
392
|
+
this.i++;
|
|
393
|
+
const ends = this.tok.t === 'eof' || (this.tok.t === 'kw' && ['end', 'endif', 'endfor', 'endfunc', 'endwhile', 'else', 'elseif'].includes(this.tok.v));
|
|
394
|
+
return ends ? { t: 'lit', pos: t.pos } : { t: 'block', parts: [this.simple()], pos: t.pos };
|
|
395
|
+
}
|
|
396
|
+
default:
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
const e = this.simple();
|
|
401
|
+
if (this.isOp('=')) {
|
|
402
|
+
if (e.t !== 'ref' || e.segs[e.segs.length - 1]?.call)
|
|
403
|
+
this.fail('cannot assign to this');
|
|
404
|
+
this.i++;
|
|
405
|
+
return { t: 'assign', target: e, value: this.simple(), pos: e.pos };
|
|
406
|
+
}
|
|
407
|
+
return e;
|
|
408
|
+
}
|
|
409
|
+
finally {
|
|
410
|
+
this.leave();
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
simple() {
|
|
414
|
+
return this.binary(0);
|
|
415
|
+
}
|
|
416
|
+
static LEVELS = [
|
|
417
|
+
['|', 'or'],
|
|
418
|
+
['&', 'and'],
|
|
419
|
+
['==', '<>', 'eq', 'ne'],
|
|
420
|
+
['<', '>', '<=', '>=', 'lt', 'gt', 'le', 'ge'],
|
|
421
|
+
['+', '-'],
|
|
422
|
+
['*', '/'],
|
|
423
|
+
];
|
|
424
|
+
binary(level) {
|
|
425
|
+
if (level >= Parser.LEVELS.length)
|
|
426
|
+
return this.unary();
|
|
427
|
+
this.enter();
|
|
428
|
+
try {
|
|
429
|
+
let l = this.binary(level + 1);
|
|
430
|
+
const ops = Parser.LEVELS[level];
|
|
431
|
+
while ((this.tok.t === 'op' || this.tok.t === 'kw') && ops.includes(this.tok.v)) {
|
|
432
|
+
const pos = this.next().pos;
|
|
433
|
+
l = { t: 'bin', l, r: this.binary(level + 1), pos };
|
|
434
|
+
}
|
|
435
|
+
return l;
|
|
436
|
+
}
|
|
437
|
+
finally {
|
|
438
|
+
this.leave();
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
unary() {
|
|
442
|
+
this.enter();
|
|
443
|
+
try {
|
|
444
|
+
if (this.isOp('-') || this.isOp('+') || this.isKw('not')) {
|
|
445
|
+
const pos = this.next().pos;
|
|
446
|
+
return { t: 'un', arg: this.unary(), pos };
|
|
447
|
+
}
|
|
448
|
+
return this.primary();
|
|
449
|
+
}
|
|
450
|
+
finally {
|
|
451
|
+
this.leave();
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
args() {
|
|
455
|
+
const out = [];
|
|
456
|
+
if (!this.isOp(')')) {
|
|
457
|
+
out.push(this.simple());
|
|
458
|
+
while (this.isOp(',')) {
|
|
459
|
+
this.i++;
|
|
460
|
+
out.push(this.simple());
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
this.expectOp(')');
|
|
464
|
+
return out;
|
|
465
|
+
}
|
|
466
|
+
primary() {
|
|
467
|
+
const t = this.tok;
|
|
468
|
+
if (t.t === 'num') {
|
|
469
|
+
this.i++;
|
|
470
|
+
return { t: 'num', value: Number(t.v), pos: t.pos };
|
|
471
|
+
}
|
|
472
|
+
if (t.t === 'str') {
|
|
473
|
+
this.i++;
|
|
474
|
+
return this.postfix({ t: 'str', value: t.v, raw: t.raw ?? '', pos: t.pos });
|
|
475
|
+
}
|
|
476
|
+
if (t.t === 'kw' && (t.v === 'null' || t.v === 'infinity' || t.v === 'nan')) {
|
|
477
|
+
this.i++;
|
|
478
|
+
return { t: 'lit', pos: t.pos };
|
|
479
|
+
}
|
|
480
|
+
if (this.isOp('(')) {
|
|
481
|
+
this.i++;
|
|
482
|
+
const e = this.simple();
|
|
483
|
+
this.expectOp(')');
|
|
484
|
+
return this.postfix(e);
|
|
485
|
+
}
|
|
486
|
+
if (t.t === 'id' || (t.t === 'kw' && t.v === 'this')) {
|
|
487
|
+
this.i++;
|
|
488
|
+
if (t.t === 'id' && this.isOp('(') && !/^[$!#]/.test(t.v)) {
|
|
489
|
+
this.i++;
|
|
490
|
+
return this.postfix({ t: 'call', name: t.v, args: this.args(), pos: t.pos });
|
|
491
|
+
}
|
|
492
|
+
const root = { name: t.v, pos: t.pos };
|
|
493
|
+
const ref = { t: 'ref', segs: [root], pos: t.pos };
|
|
494
|
+
this.segments(ref);
|
|
495
|
+
return ref;
|
|
496
|
+
}
|
|
497
|
+
return this.fail('unexpected');
|
|
498
|
+
}
|
|
499
|
+
/** Accessors after a value that is not a name, such as a call's result. */
|
|
500
|
+
postfix(e) {
|
|
501
|
+
if (!this.isOp('.') && !this.isOp('..') && !this.isOp('['))
|
|
502
|
+
return e;
|
|
503
|
+
const ref = { t: 'ref', base: e, segs: [], pos: e.pos };
|
|
504
|
+
this.segments(ref);
|
|
505
|
+
return ref;
|
|
506
|
+
}
|
|
507
|
+
segments(ref) {
|
|
508
|
+
for (;;) {
|
|
509
|
+
const last = ref.segs[ref.segs.length - 1];
|
|
510
|
+
if (this.isOp('[')) {
|
|
511
|
+
this.i++;
|
|
512
|
+
let index;
|
|
513
|
+
if (this.isOp('*')) {
|
|
514
|
+
index = { t: 'lit', pos: this.next().pos };
|
|
515
|
+
}
|
|
516
|
+
else
|
|
517
|
+
index = this.simple();
|
|
518
|
+
this.expectOp(']');
|
|
519
|
+
if (last)
|
|
520
|
+
last.index = index;
|
|
521
|
+
else
|
|
522
|
+
ref.segs.push({ name: '[]', pos: index.pos, index, select: true });
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
if (this.isOp('.') || this.isOp('..')) {
|
|
526
|
+
const descend = this.next().v === '..';
|
|
527
|
+
const pos = this.tok.pos;
|
|
528
|
+
if (this.isOp('*')) {
|
|
529
|
+
this.i++;
|
|
530
|
+
ref.segs.push({ name: '*', pos, select: true });
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
if (this.isOp('[')) {
|
|
534
|
+
this.i++;
|
|
535
|
+
const index = this.simple();
|
|
536
|
+
this.expectOp(']');
|
|
537
|
+
ref.segs.push({ name: '[]', pos, index, select: true });
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
if (this.tok.t !== 'id' && this.tok.t !== 'kw')
|
|
541
|
+
this.fail('expected a name');
|
|
542
|
+
const seg = { name: this.next().v, pos, select: descend };
|
|
543
|
+
if (this.isOp('(')) {
|
|
544
|
+
this.i++;
|
|
545
|
+
seg.call = this.args();
|
|
546
|
+
}
|
|
547
|
+
ref.segs.push(seg);
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
class DepthError extends Error {
|
|
555
|
+
}
|
|
556
|
+
function analyzeFormCalc(input) {
|
|
557
|
+
const options = input.options ?? {};
|
|
558
|
+
const overrides = options.actionOverrides ?? [];
|
|
559
|
+
const findings = [];
|
|
560
|
+
const reported = new Set();
|
|
561
|
+
const where = (pos) => {
|
|
562
|
+
const line = pos === undefined ? '' : `line ${lineOf(input.text, pos)}`;
|
|
563
|
+
const ctx = input.context?.location ?? (input.context?.fieldName ? `field ${JSON.stringify(input.context.fieldName)}` : input.context?.trigger);
|
|
564
|
+
return [ctx, line].filter(Boolean).join(', ') || undefined;
|
|
565
|
+
};
|
|
566
|
+
const report = (c, d, pos, data) => {
|
|
567
|
+
const key = `${c}/${d}/${String(data?.api ?? '')}`;
|
|
568
|
+
if (reported.has(key))
|
|
569
|
+
return;
|
|
570
|
+
reported.add(key);
|
|
571
|
+
findings.push((0, findings_1.makeFinding)(c, d, overrides, where(pos), data));
|
|
572
|
+
};
|
|
573
|
+
const reportMember = (m, api, pos) => {
|
|
574
|
+
if (m && 'finding' in m)
|
|
575
|
+
report(m.finding[0], m.finding[1], pos, { api });
|
|
576
|
+
};
|
|
577
|
+
const done = () => {
|
|
578
|
+
if (!findings.some(f => f.action === 'strip'))
|
|
579
|
+
report(types_1.ScriptCategory.Form, types_1.ScriptDetail.Computation);
|
|
580
|
+
return { findings, definedGlobals: [] };
|
|
581
|
+
};
|
|
582
|
+
const limits = options.limits ?? {};
|
|
583
|
+
if (limits.scriptSize !== undefined && input.text.length > limits.scriptSize) {
|
|
584
|
+
report(types_1.ScriptCategory.Limit, types_1.ScriptDetail.Size, undefined, { length: input.text.length });
|
|
585
|
+
return { findings, definedGlobals: [] };
|
|
586
|
+
}
|
|
587
|
+
let program;
|
|
588
|
+
try {
|
|
589
|
+
const parser = new Parser(tokenize(input.text), limits.depth ?? Number.POSITIVE_INFINITY);
|
|
590
|
+
program = parser.program();
|
|
591
|
+
}
|
|
592
|
+
catch (err) {
|
|
593
|
+
if (err instanceof DepthError)
|
|
594
|
+
report(types_1.ScriptCategory.Limit, types_1.ScriptDetail.Depth, undefined, { depth: limits.depth ?? 0 });
|
|
595
|
+
else
|
|
596
|
+
report(types_1.ScriptCategory.Syntax, types_1.ScriptDetail.Unparseable, undefined, { reason: err instanceof Error ? err.message : String(err) });
|
|
597
|
+
return { findings, definedGlobals: [] };
|
|
598
|
+
}
|
|
599
|
+
// Names the script declares: functions, variables, parameters and loop variables. They hold values, not nodes.
|
|
600
|
+
const funcs = new Set();
|
|
601
|
+
const vars = new Set();
|
|
602
|
+
const declare = (e) => {
|
|
603
|
+
if (e.t === 'func') {
|
|
604
|
+
funcs.add(e.name.toLowerCase());
|
|
605
|
+
for (const p of e.params)
|
|
606
|
+
vars.add(p);
|
|
607
|
+
e.body.forEach(declare);
|
|
608
|
+
}
|
|
609
|
+
else if (e.t === 'var') {
|
|
610
|
+
vars.add(e.name);
|
|
611
|
+
if (e.init)
|
|
612
|
+
declare(e.init);
|
|
613
|
+
}
|
|
614
|
+
else
|
|
615
|
+
for (const c of childrenOf(e))
|
|
616
|
+
declare(c);
|
|
617
|
+
};
|
|
618
|
+
program.forEach(declare);
|
|
619
|
+
const describe = (ref, upTo = ref.segs.length) => {
|
|
620
|
+
const head = ref.base ? (ref.base.t === 'call' ? `${ref.base.name}()` : '(...)') : '';
|
|
621
|
+
const parts = ref.segs.slice(0, upTo).map((s, i) => `${i === 0 && !ref.base ? '' : '.'}${s.name}${s.call ? '()' : ''}`);
|
|
622
|
+
return head + parts.join('');
|
|
623
|
+
};
|
|
624
|
+
/** The kind of a member of a value of `kind`, reporting what the member reaches. */
|
|
625
|
+
const member = (kind, name, api, pos, write) => {
|
|
626
|
+
const t = TABLES.get(kind);
|
|
627
|
+
if (t) {
|
|
628
|
+
const m = t.get(name);
|
|
629
|
+
if (!m) {
|
|
630
|
+
report(types_1.ScriptCategory.Api, types_1.ScriptDetail.Unknown, pos, { api });
|
|
631
|
+
return 'node';
|
|
632
|
+
}
|
|
633
|
+
if ('finding' in m) {
|
|
634
|
+
reportMember(m, api, pos);
|
|
635
|
+
return 'node';
|
|
636
|
+
}
|
|
637
|
+
if (write && !m.write)
|
|
638
|
+
report(types_1.ScriptCategory.Api, types_1.ScriptDetail.Unknown, pos, { api: `${api} =` });
|
|
639
|
+
return m.kind;
|
|
640
|
+
}
|
|
641
|
+
const deny = api_1.HOST_DENY.get(name) ?? api_1.NODE_DENY.get(name);
|
|
642
|
+
if (deny)
|
|
643
|
+
reportMember(deny, api, pos);
|
|
644
|
+
else if (write && api_1.URL_ATTRIBUTES.has(name))
|
|
645
|
+
report(types_1.ScriptCategory.External, types_1.ScriptDetail.Url, pos, { api: `${api} =` });
|
|
646
|
+
return api_1.NODE_VALUES.get(name) ?? 'node';
|
|
647
|
+
};
|
|
648
|
+
const rootKind = (name, pos) => {
|
|
649
|
+
if (vars.has(name))
|
|
650
|
+
return 'data';
|
|
651
|
+
if (name === 'xfa')
|
|
652
|
+
return 'xfa';
|
|
653
|
+
if (name === 'this')
|
|
654
|
+
return 'node';
|
|
655
|
+
const som = api_1.SOM_ROOTS.get(name);
|
|
656
|
+
if (som !== undefined) {
|
|
657
|
+
if (typeof som === 'string')
|
|
658
|
+
return som;
|
|
659
|
+
reportMember(som, name, pos);
|
|
660
|
+
return 'node';
|
|
661
|
+
}
|
|
662
|
+
if (name.startsWith('!'))
|
|
663
|
+
return 'node';
|
|
664
|
+
if (name.startsWith('$')) {
|
|
665
|
+
report(types_1.ScriptCategory.Api, types_1.ScriptDetail.Unknown, pos, { api: name });
|
|
666
|
+
return 'node';
|
|
667
|
+
}
|
|
668
|
+
// A form node by name, unless it is a name a viewer object has that leaves the form.
|
|
669
|
+
const deny = api_1.HOST_DENY.get(name) ?? api_1.NODE_DENY.get(name);
|
|
670
|
+
if (deny)
|
|
671
|
+
reportMember(deny, name, pos);
|
|
672
|
+
return 'node';
|
|
673
|
+
};
|
|
674
|
+
const checkStrings = (e) => {
|
|
675
|
+
const s = e.value;
|
|
676
|
+
if (analyze_1.ENCODED.some(re => re.test(s)) || analyze_1.ENCODED.some(re => re.test(e.raw)))
|
|
677
|
+
report(types_1.ScriptCategory.Obfuscation, types_1.ScriptDetail.EncodedString, e.pos, { length: s.length });
|
|
678
|
+
else if ((0, analyze_1.isLongBlob)(s))
|
|
679
|
+
report(types_1.ScriptCategory.Obfuscation, types_1.ScriptDetail.LongString, e.pos, { length: s.length });
|
|
680
|
+
const escapes = e.raw.match(NEEDLESS_ESCAPE)?.length ?? 0;
|
|
681
|
+
if (escapes >= 3)
|
|
682
|
+
report(types_1.ScriptCategory.Obfuscation, types_1.ScriptDetail.Escapes, e.pos, { count: escapes });
|
|
683
|
+
};
|
|
684
|
+
const literalOf = (e) => (e?.t === 'str' ? e.value : undefined);
|
|
685
|
+
const visitRef = (ref, write) => {
|
|
686
|
+
let kind = 'data';
|
|
687
|
+
if (ref.base)
|
|
688
|
+
visit(ref.base);
|
|
689
|
+
ref.segs.forEach((seg, i) => {
|
|
690
|
+
if (seg.index)
|
|
691
|
+
visit(seg.index);
|
|
692
|
+
for (const a of seg.call ?? [])
|
|
693
|
+
visit(a);
|
|
694
|
+
const api = describe(ref, i + 1);
|
|
695
|
+
const isWrite = write && i === ref.segs.length - 1;
|
|
696
|
+
if (i === 0 && !ref.base)
|
|
697
|
+
kind = rootKind(seg.name, seg.pos);
|
|
698
|
+
else if (seg.select)
|
|
699
|
+
kind = 'node';
|
|
700
|
+
else {
|
|
701
|
+
const parent = kind;
|
|
702
|
+
kind = member(parent, seg.name, api.replace(/\(\)$/, ''), seg.pos, isWrite);
|
|
703
|
+
// $host.response(question, title, default, 1): a password asked for in a prompt.
|
|
704
|
+
if (seg.call && parent === 'xfahost' && seg.name === 'response') {
|
|
705
|
+
const flag = seg.call[3];
|
|
706
|
+
if (flag && !(flag.t === 'num' && flag.value === 0) && flag.t !== 'lit')
|
|
707
|
+
report(types_1.ScriptCategory.Api, types_1.ScriptDetail.PasswordPrompt, seg.pos, { api: api.replace(/\(\)$/, '') });
|
|
708
|
+
}
|
|
709
|
+
if (seg.call && (parent === 'node' || parent === 'data' || parent === 'xfa')) {
|
|
710
|
+
const receiver = describe(ref, i);
|
|
711
|
+
const first = literalOf(seg.call[0]);
|
|
712
|
+
if (seg.name === 'createNode' && (first === undefined || api_1.CODE_NODES.has(first)))
|
|
713
|
+
report(types_1.ScriptCategory.DynamicCode, types_1.ScriptDetail.InstallScript, seg.pos, { api: `${api.replace(/\(\)$/, '')}(${first === undefined ? '...' : JSON.stringify(first)})` });
|
|
714
|
+
if (DATA_ONLY.has(seg.name) && !DATA_PATH.test(receiver))
|
|
715
|
+
report(types_1.ScriptCategory.DynamicCode, types_1.ScriptDetail.InstallScript, seg.pos, { api: api.replace(/\(\)$/, '') });
|
|
716
|
+
if (seg.name === 'setAttribute') {
|
|
717
|
+
const attr = literalOf(seg.call[1]);
|
|
718
|
+
if (attr === undefined || api_1.URL_ATTRIBUTES.has(attr))
|
|
719
|
+
report(types_1.ScriptCategory.External, types_1.ScriptDetail.Url, seg.pos, { api: `${api.replace(/\(\)$/, '')}(..., ${attr === undefined ? '...' : JSON.stringify(attr)})` });
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
};
|
|
725
|
+
const visit = (e) => {
|
|
726
|
+
switch (e.t) {
|
|
727
|
+
case 'str':
|
|
728
|
+
checkStrings(e);
|
|
729
|
+
return;
|
|
730
|
+
case 'ref':
|
|
731
|
+
visitRef(e, false);
|
|
732
|
+
return;
|
|
733
|
+
case 'assign':
|
|
734
|
+
visitRef(e.target, true);
|
|
735
|
+
visit(e.value);
|
|
736
|
+
return;
|
|
737
|
+
case 'call': {
|
|
738
|
+
const name = e.name.toLowerCase();
|
|
739
|
+
e.args.forEach(visit);
|
|
740
|
+
const denied = DENIED_FUNCTIONS.get(name);
|
|
741
|
+
if (denied)
|
|
742
|
+
report(denied[0], denied[1], e.pos, { api: e.name });
|
|
743
|
+
else if (!FUNCTIONS.has(name) && !funcs.has(name))
|
|
744
|
+
report(types_1.ScriptCategory.Api, types_1.ScriptDetail.Unknown, e.pos, { api: e.name });
|
|
745
|
+
if (name === 'concat') {
|
|
746
|
+
const literals = e.args.filter(a => a.t === 'str');
|
|
747
|
+
const tiny = literals.filter(a => a.t === 'str' && a.value.length <= 2).length;
|
|
748
|
+
if (tiny >= 6 || literals.length >= 25)
|
|
749
|
+
report(types_1.ScriptCategory.Obfuscation, types_1.ScriptDetail.Concatenation, e.pos, { count: e.args.length });
|
|
750
|
+
}
|
|
751
|
+
// Replace("qwqoqrqd", "q", ""): text hidden among filler in a string the script wrote itself.
|
|
752
|
+
if (name === 'replace' && (literalOf(e.args[0])?.length ?? 0) > 3)
|
|
753
|
+
report(types_1.ScriptCategory.Obfuscation, types_1.ScriptDetail.Decode, e.pos, { api: 'Replace("...")' });
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
default:
|
|
757
|
+
for (const c of childrenOf(e))
|
|
758
|
+
visit(c);
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
program.forEach(visit);
|
|
762
|
+
return done();
|
|
763
|
+
}
|
|
764
|
+
function childrenOf(e) {
|
|
765
|
+
switch (e.t) {
|
|
766
|
+
case 'ref':
|
|
767
|
+
return [...(e.base ? [e.base] : []), ...e.segs.flatMap(s => [...(s.index ? [s.index] : []), ...(s.call ?? [])])];
|
|
768
|
+
case 'call':
|
|
769
|
+
return e.args;
|
|
770
|
+
case 'un':
|
|
771
|
+
return [e.arg];
|
|
772
|
+
case 'bin':
|
|
773
|
+
return [e.l, e.r];
|
|
774
|
+
case 'assign':
|
|
775
|
+
return [e.target, e.value];
|
|
776
|
+
case 'var':
|
|
777
|
+
return e.init ? [e.init] : [];
|
|
778
|
+
case 'func':
|
|
779
|
+
return e.body;
|
|
780
|
+
case 'block':
|
|
781
|
+
return e.parts;
|
|
782
|
+
default:
|
|
783
|
+
return [];
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
function lineOf(text, offset) {
|
|
787
|
+
let line = 1;
|
|
788
|
+
for (let i = 0; i < offset && i < text.length; i++)
|
|
789
|
+
if (text.charCodeAt(i) === 10)
|
|
790
|
+
line++;
|
|
791
|
+
return line;
|
|
792
|
+
}
|