@contrast/protect 1.38.0 → 1.40.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.
Files changed (58) hide show
  1. package/lib/error-handlers/common-handler.test.js +52 -0
  2. package/lib/error-handlers/index.test.js +32 -0
  3. package/lib/error-handlers/init-domain.test.js +34 -0
  4. package/lib/error-handlers/install/express4.test.js +238 -0
  5. package/lib/error-handlers/install/fastify.test.js +130 -0
  6. package/lib/error-handlers/install/hapi.test.js +102 -0
  7. package/lib/error-handlers/install/koa2.test.js +83 -0
  8. package/lib/error-handlers/install/restify.test.js +57 -0
  9. package/lib/get-source-context.test.js +35 -0
  10. package/lib/hardening/handlers.test.js +89 -0
  11. package/lib/hardening/index.test.js +31 -0
  12. package/lib/hardening/install/node-serialize0.test.js +61 -0
  13. package/lib/index.test.js +53 -0
  14. package/lib/input-analysis/handlers.test.js +1604 -0
  15. package/lib/input-analysis/index.test.js +45 -0
  16. package/lib/input-analysis/install/body-parser1.test.js +134 -0
  17. package/lib/input-analysis/install/busboy1.test.js +81 -0
  18. package/lib/input-analysis/install/cookie-parser1.test.js +144 -0
  19. package/lib/input-analysis/install/express4.test.js +208 -0
  20. package/lib/input-analysis/install/fastify.test.js +96 -0
  21. package/lib/input-analysis/install/formidable1.test.js +114 -0
  22. package/lib/input-analysis/install/hapi.test.js +300 -0
  23. package/lib/input-analysis/install/http.test.js +264 -0
  24. package/lib/input-analysis/install/koa-body5.test.js +92 -0
  25. package/lib/input-analysis/install/koa-bodyparser4.test.js +92 -0
  26. package/lib/input-analysis/install/koa2.test.js +259 -0
  27. package/lib/input-analysis/install/multer1.test.js +209 -0
  28. package/lib/input-analysis/install/qs6.test.js +79 -0
  29. package/lib/input-analysis/install/restify.test.js +98 -0
  30. package/lib/input-analysis/install/universal-cookie4.test.js +70 -0
  31. package/lib/input-analysis/ip-analysis.test.js +71 -0
  32. package/lib/input-analysis/virtual-patches.test.js +106 -0
  33. package/lib/input-tracing/handlers/index.test.js +1236 -0
  34. package/lib/input-tracing/index.test.js +62 -0
  35. package/lib/input-tracing/install/child-process.test.js +133 -0
  36. package/lib/input-tracing/install/eval.test.js +78 -0
  37. package/lib/input-tracing/install/fs.test.js +108 -0
  38. package/lib/input-tracing/install/function.test.js +81 -0
  39. package/lib/input-tracing/install/http.test.js +85 -0
  40. package/lib/input-tracing/install/http2.test.js +83 -0
  41. package/lib/input-tracing/install/marsdb.test.js +126 -0
  42. package/lib/input-tracing/install/mongodb.test.js +282 -0
  43. package/lib/input-tracing/install/mssql.test.js +81 -0
  44. package/lib/input-tracing/install/mysql.test.js +108 -0
  45. package/lib/input-tracing/install/postgres.test.js +125 -0
  46. package/lib/input-tracing/install/sequelize.test.js +78 -0
  47. package/lib/input-tracing/install/spdy.test.js +76 -0
  48. package/lib/input-tracing/install/sqlite3.test.js +88 -0
  49. package/lib/input-tracing/install/vm.test.js +176 -0
  50. package/lib/make-response-blocker.test.js +99 -0
  51. package/lib/make-source-context.test.js +219 -0
  52. package/lib/policy.test.js +446 -0
  53. package/lib/semantic-analysis/handlers.test.js +379 -0
  54. package/lib/semantic-analysis/index.test.js +38 -0
  55. package/lib/semantic-analysis/install/libxmljs.test.js +156 -0
  56. package/lib/semantic-analysis/utils/xml-analysis.test.js +156 -0
  57. package/lib/throw-security-exception.test.js +37 -0
  58. package/package.json +5 -5
@@ -0,0 +1,1236 @@
1
+ 'use strict';
2
+
3
+ const { expect } = require('chai');
4
+ const agentLib = require('@contrast/agent-lib');
5
+ const mocks = require('@contrast/test/mocks');
6
+ const { agentLibIDListTypes } = require('@contrast/common');
7
+
8
+ describe('protect input-tracing handlers', function () {
9
+ let handlers;
10
+
11
+ // each test should be asserting this at least
12
+ const TEST_DESCRIPTION = 'captures findings and sinkContext in result.exploitMetadata array';
13
+
14
+ // for additional assertions that might happen in tests, add to description
15
+ function makeTestDescription(blocks) {
16
+ const addl = blocks ? ' and blocks when in BLOCK mode' : '';
17
+ return `${TEST_DESCRIPTION}${addl}`;
18
+ }
19
+
20
+ function makeSourceContext(resultsList, ruleModes) {
21
+ const resultsMap = Object.create(null);
22
+
23
+ if (resultsList) {
24
+ for (const result of resultsList) {
25
+ if (!resultsMap[result.ruleId]) {
26
+ resultsMap[result.ruleId] = [];
27
+ }
28
+ resultsMap[result.ruleId].push(result);
29
+ }
30
+ }
31
+
32
+ const testRuleIds = [
33
+ 'cmd-injection',
34
+ 'path-traversal',
35
+ 'sql-injection',
36
+ 'ssjs-injection',
37
+ 'nosql-injection',
38
+ 'nosql-injection-mongo',
39
+ 'reflected-xss',
40
+ ];
41
+
42
+ return {
43
+ policy: {
44
+ rulesMask: testRuleIds.reduce((acc, ruleId) => acc | agentLib.constants.RuleType[ruleId]),
45
+ ...testRuleIds.reduce((acc, ruleId) => ({ ...acc, [ruleId]: 'monitor' }), {}),
46
+ ...ruleModes,
47
+ },
48
+ trackRequest: true,
49
+ resultsMap,
50
+ };
51
+ }
52
+
53
+ beforeEach(function () {
54
+ const core = mocks.core();
55
+ core.logger = mocks.logger();
56
+ core.protect = mocks.protect();
57
+ core.protect.throwSecurityException = require('../../throw-security-exception');
58
+
59
+ handlers = require('.')(core);
60
+ });
61
+
62
+ describe('handlePathTraversal()', function () {
63
+ const sinkContextPositive = {
64
+ name: 'fs.readFileSync',
65
+ value: './../../../etc/passwd',
66
+ stack: []
67
+ };
68
+ const sinkContextNegative = {
69
+ name: 'fs.readFileSync',
70
+ value: 'index.js',
71
+ stack: []
72
+ };
73
+ const findings = {
74
+ path: './../../../etc/passwd',
75
+ };
76
+
77
+ [
78
+ {
79
+ blocks: false,
80
+ sourceContext: makeSourceContext(
81
+ [{
82
+ ruleId: 'path-traversal',
83
+ value: '../../../etc/passwd',
84
+ exploitMetadata: [],
85
+ }]
86
+ ),
87
+ expectedDetails: [
88
+ {
89
+ sinkContext: sinkContextPositive,
90
+ findings,
91
+ },
92
+ {
93
+ sinkContext: sinkContextPositive,
94
+ findings,
95
+ }
96
+ ]
97
+ },
98
+ {
99
+ blocks: true,
100
+ sourceContext: makeSourceContext(
101
+ [{
102
+ ruleId: 'path-traversal',
103
+ value: '../../../etc/passwd',
104
+ exploitMetadata: [],
105
+ }],
106
+ {
107
+ 'path-traversal': 'block'
108
+ }
109
+ ),
110
+ expectedDetails: [
111
+ {
112
+ sinkContext: sinkContextPositive,
113
+ findings,
114
+ },
115
+ ]
116
+ },
117
+ {
118
+ blocks: false,
119
+ sourceContext: makeSourceContext(
120
+ [{
121
+ ruleId: 'path-traversal',
122
+ value: '../../../etc/passwd',
123
+ exploitMetadata: [],
124
+ }],
125
+ {
126
+ ['path-traversal']: 'off'
127
+ }
128
+ ),
129
+ expectedDetails: []
130
+ }
131
+ ].forEach(({ blocks, sourceContext, expectedDetails }) => {
132
+ it(makeTestDescription(blocks), function () {
133
+ const testFn = () => {
134
+ handlers.handlePathTraversal(sourceContext, sinkContextPositive);
135
+ handlers.handlePathTraversal(sourceContext, sinkContextPositive);
136
+ handlers.handlePathTraversal(sourceContext, sinkContextNegative);
137
+ };
138
+
139
+ const test = expect(testFn);
140
+ blocks ? test.to.throw('SecurityException') : test.not.to.throw();
141
+
142
+ expect(sourceContext.resultsMap['path-traversal'][0].exploitMetadata).to.deep.equal(expectedDetails);
143
+ });
144
+ });
145
+ });
146
+
147
+ describe('handleCmdInjection()', function () {
148
+ const sinkContextPositive = {
149
+ name: 'child_process.execSync',
150
+ value: 'ls; cat /etc/passwd',
151
+ stack: [],
152
+ };
153
+ const sinkContextNegative = {
154
+ name: 'child_process.execSync',
155
+ value: 'foo',
156
+ stack: []
157
+ };
158
+ const findings = {
159
+ boundaryIndex: 2,
160
+ endIndex: 19,
161
+ overrunIndex: 4,
162
+ startIndex: 2,
163
+ };
164
+
165
+ [
166
+ {
167
+ blocks: false,
168
+ sourceContext: makeSourceContext(
169
+ [{
170
+ ruleId: 'cmd-injection',
171
+ value: '; cat /etc/passwd',
172
+ exploitMetadata: [],
173
+ }]
174
+ ),
175
+ expectedDetails: [
176
+ {
177
+ sinkContext: sinkContextPositive,
178
+ findings,
179
+ },
180
+ {
181
+ sinkContext: sinkContextPositive,
182
+ findings,
183
+ }
184
+ ]
185
+ },
186
+ {
187
+ blocks: true,
188
+ sourceContext: makeSourceContext(
189
+ [{
190
+ ruleId: 'cmd-injection',
191
+ value: '; cat /etc/passwd',
192
+ exploitMetadata: [],
193
+ }], {
194
+ ['cmd-injection']: 'block'
195
+ }
196
+ ),
197
+ expectedDetails: [
198
+ {
199
+ sinkContext: sinkContextPositive,
200
+ findings,
201
+ },
202
+ ]
203
+ },
204
+ {
205
+ blocks: false,
206
+ sourceContext: makeSourceContext(
207
+ [{
208
+ ruleId: 'cmd-injection',
209
+ value: '; cat /etc/passwd',
210
+ exploitMetadata: [],
211
+ }], {
212
+ ['cmd-injection']: 'off'
213
+ }
214
+ ),
215
+ expectedDetails: []
216
+ }
217
+ ].forEach(({ blocks, sourceContext, expectedDetails }) => {
218
+ it(makeTestDescription(blocks), function () {
219
+ const testFn = () => {
220
+ handlers.handleCommandInjection(sourceContext, sinkContextPositive);
221
+ handlers.handleCommandInjection(sourceContext, sinkContextPositive);
222
+ handlers.handleCommandInjection(sourceContext, sinkContextNegative);
223
+ };
224
+
225
+ const test = expect(testFn);
226
+ blocks ? test.to.throw('SecurityException') : test.not.to.throw();
227
+
228
+ const cmdiResults = sourceContext.resultsMap['cmd-injection'];
229
+
230
+ expect(cmdiResults[0].exploitMetadata).to.deep.equal(expectedDetails);
231
+ });
232
+ });
233
+ });
234
+
235
+ describe('handleSqlInjection()', function () {
236
+ const sinkContextPositive = {
237
+ name: 'mysql.query',
238
+ value: 'select * from foo where col = """ and 1 = 1; --" or col = "" and 1 = 1; --',
239
+ stack: [],
240
+ };
241
+ const sinkContextNegative = {
242
+ name: 'mysql.query',
243
+ value: 'select * from foo where col = """ and 1 = 1; --"',
244
+ stack: [],
245
+ };
246
+ const findings = {
247
+ boundaryIndex: 59,
248
+ endIndex: 74,
249
+ overrunIndex: 61,
250
+ startIndex: 59,
251
+ };
252
+
253
+ [
254
+ {
255
+ blocks: false,
256
+ findings,
257
+ sourceContext: makeSourceContext(
258
+ [
259
+ {
260
+ ruleId: 'sql-injection',
261
+ value: '" and 1 = 1; --',
262
+ exploitMetadata: [],
263
+ }
264
+ ]
265
+ ),
266
+ expectedDetails: [
267
+ {
268
+ sinkContext: sinkContextPositive,
269
+ findings,
270
+ },
271
+ {
272
+ sinkContext: sinkContextPositive,
273
+ findings,
274
+ }
275
+ ]
276
+ },
277
+ {
278
+ blocks: true,
279
+ findings: {
280
+ startIndex: 0,
281
+ endIndex: 30,
282
+ overrunIndex: 0,
283
+ boundaryIndex: 0,
284
+ },
285
+ sourceContext: makeSourceContext(
286
+ [
287
+ {
288
+ ruleId: 'sql-injection',
289
+ // this is the exact value of the query - this is to get coverage
290
+ value: 'select * from foo where col = """ and 1 = 1; --" or col = "" and 1 = 1; --',
291
+ exploitMetadata: [],
292
+ }
293
+ ],
294
+ {
295
+ ['sql-injection']: 'block'
296
+ }
297
+ ),
298
+ expectedDetails: [
299
+ {
300
+ sinkContext: sinkContextPositive,
301
+ findings: {
302
+ startIndex: 0,
303
+ endIndex: 73,
304
+ overrunIndex: 0,
305
+ boundaryIndex: 0,
306
+ },
307
+ },
308
+ ]
309
+ },
310
+ {
311
+ blocks: false,
312
+ findings: {
313
+ startIndex: 0,
314
+ endIndex: 30,
315
+ overrunIndex: 0,
316
+ boundaryIndex: 0,
317
+ },
318
+ sourceContext: makeSourceContext(
319
+ [
320
+ {
321
+ ruleId: 'sql-injection',
322
+ value: '" and 1 = 1; --',
323
+ exploitMetadata: [],
324
+ }
325
+ ],
326
+ {
327
+ ['sql-injection']: 'off'
328
+ }
329
+ ),
330
+ expectedDetails: []
331
+ }
332
+ ].forEach(({ blocks, sourceContext, expectedDetails }) => {
333
+ it(makeTestDescription(blocks), function () {
334
+ const testFn = () => {
335
+ handlers.handleSqlInjection(sourceContext, sinkContextPositive);
336
+ handlers.handleSqlInjection(sourceContext, sinkContextPositive);
337
+ handlers.handleSqlInjection(sourceContext, sinkContextNegative);
338
+ };
339
+
340
+ const test = expect(testFn);
341
+ blocks ? test.to.throw('SecurityException') : test.not.to.throw();
342
+
343
+ const sqliResults = sourceContext.resultsMap['sql-injection'];
344
+ expect(sqliResults[0].exploitMetadata).to.deep.equal(expectedDetails);
345
+ });
346
+ });
347
+ });
348
+
349
+ describe('nosqlInjectionMongo()', function () {
350
+ const expansionSinkContextPositive = {
351
+ name: 'mongodb.find',
352
+ value: { id: { '$ne': 0 }, name: 'testValue' },
353
+ stack: [],
354
+ };
355
+ const expansionSinkContextNegative = {
356
+ name: 'mongodb.find',
357
+ value: { id: 1 },
358
+ stack: [],
359
+ };
360
+ const expansionSinkContextWrong = {
361
+ name: 'mongodb.find',
362
+ value: '{ id: { "$ne": 0 }, name: "testValue" }',
363
+ stack: [],
364
+ };
365
+ const expansionFindings = {
366
+ start: 8,
367
+ end: 11,
368
+ inputBoundaryIndex: 0,
369
+ boundaryOverrunIndex: 8
370
+ };
371
+ const stringSinkContextPositive = {
372
+ name: 'mongodb.Db.eval',
373
+ value: 'function() { console.log(`function() { return "hi"; }`) (function() { return "hi"; })() }',
374
+ stack: [],
375
+ };
376
+ const stringSinkContextNegative = {
377
+ name: 'mongodb.Db.eval',
378
+ value: 'function() { console.log(`function() { return "hi"; }`) }',
379
+ stack: [],
380
+ };
381
+ const stringSinkContextWrong = {
382
+ name: 'mongodb.Db.eval',
383
+ value: { id: { '$ne': 0 }, name: 'testValue' },
384
+ stack: [],
385
+ };
386
+ const stringFindings = {
387
+ start: 58,
388
+ end: 84,
389
+ boundaryOverrunIndex: 0,
390
+ inputBoundaryIndex: 0,
391
+ };
392
+
393
+ [
394
+ {
395
+ blocks: false,
396
+ type: 'expansionInjection',
397
+ findings: expansionFindings,
398
+ sourceContext: makeSourceContext(
399
+ [
400
+ {
401
+ ruleId: 'nosql-injection-mongo',
402
+ value: { '$ne': 0 },
403
+ key: '$ne',
404
+ exploitMetadata: [],
405
+ mongoContext: { inputToCheck: 0 },
406
+ }
407
+ ]
408
+ ),
409
+ expectedDetails: [
410
+ {
411
+ sinkContext: expansionSinkContextPositive,
412
+ findings: expansionFindings,
413
+ },
414
+ {
415
+ sinkContext: expansionSinkContextPositive,
416
+ findings: expansionFindings,
417
+ }
418
+ ]
419
+ },
420
+ {
421
+ blocks: true,
422
+ type: 'expansionInjection',
423
+ findings: expansionFindings,
424
+ sourceContext: makeSourceContext(
425
+ [
426
+ {
427
+ ruleId: 'nosql-injection-mongo',
428
+ // this is the exact value of the query - this is to get coverage
429
+ value: { '$ne': 0 },
430
+ key: '$ne',
431
+ exploitMetadata: [],
432
+ mongoContext: { inputToCheck: 0 },
433
+ }
434
+ ],
435
+ {
436
+ ['nosql-injection-mongo']: 'block'
437
+ }
438
+ ),
439
+ expectedDetails: [
440
+ {
441
+ sinkContext: expansionSinkContextPositive,
442
+ findings: expansionFindings,
443
+ },
444
+ ]
445
+ },
446
+ {
447
+ blocks: false,
448
+ type: 'expansionInjection',
449
+ findings: expansionFindings,
450
+ sourceContext: makeSourceContext(
451
+ [
452
+ {
453
+ ruleId: 'nosql-injection-mongo',
454
+ value: { '$ne': 0 },
455
+ key: '$ne',
456
+ exploitMetadata: [],
457
+ mongoContext: { inputToCheck: 0 },
458
+ }
459
+ ],
460
+ {
461
+ ['nosql-injection-mongo']: 'off'
462
+ }
463
+ ),
464
+ expectedDetails: []
465
+ },
466
+ {
467
+ blocks: false,
468
+ type: 'stringInjection',
469
+ stringFindings,
470
+ sourceContext: makeSourceContext(
471
+ [
472
+ {
473
+ ruleId: 'ssjs-injection',
474
+ value: 'function() { return "hi"; }',
475
+ exploitMetadata: [],
476
+ }
477
+ ]
478
+ ),
479
+ expectedDetails: [
480
+ {
481
+ sinkContext: stringSinkContextPositive,
482
+ findings: stringFindings,
483
+ },
484
+ {
485
+ sinkContext: stringSinkContextPositive,
486
+ findings: stringFindings,
487
+ }
488
+ ]
489
+ },
490
+ {
491
+ blocks: true,
492
+ type: 'stringInjection',
493
+ findings: stringFindings,
494
+ sourceContext: makeSourceContext(
495
+ [
496
+ {
497
+ ruleId: 'ssjs-injection',
498
+ value: 'function() { return "hi"; }',
499
+ exploitMetadata: [],
500
+ }
501
+ ],
502
+ {
503
+ ['nosql-injection-mongo']: 'block'
504
+ }
505
+ ),
506
+ expectedDetails: [
507
+ {
508
+ sinkContext: stringSinkContextPositive,
509
+ findings: stringFindings,
510
+ },
511
+ ]
512
+ },
513
+ {
514
+ blocks: false,
515
+ type: 'stringInjection',
516
+ findings: stringFindings,
517
+ sourceContext: makeSourceContext(
518
+ [
519
+ {
520
+ ruleId: 'ssjs-injection',
521
+ value: 'function() { return "hi"; }',
522
+ exploitMetadata: [],
523
+ }
524
+ ],
525
+ {
526
+ ['ssjs-injection']: 'off'
527
+ }
528
+ ),
529
+ expectedDetails: []
530
+ },
531
+ ].forEach(({ blocks, type, sourceContext, expectedDetails }) => {
532
+ it(makeTestDescription(blocks), function () {
533
+ const testFn = () => {
534
+ if (type == 'expansionInjection') {
535
+ handlers.nosqlInjectionMongo(sourceContext, expansionSinkContextPositive);
536
+ handlers.nosqlInjectionMongo(sourceContext, expansionSinkContextPositive);
537
+ handlers.nosqlInjectionMongo(sourceContext, expansionSinkContextNegative);
538
+ handlers.nosqlInjectionMongo(sourceContext, expansionSinkContextWrong);
539
+ } else {
540
+ handlers.nosqlInjectionMongo(sourceContext, stringSinkContextPositive);
541
+ handlers.nosqlInjectionMongo(sourceContext, stringSinkContextPositive);
542
+ handlers.nosqlInjectionMongo(sourceContext, stringSinkContextNegative);
543
+ handlers.nosqlInjectionMongo(sourceContext, stringSinkContextWrong);
544
+ }
545
+ };
546
+
547
+ const test = expect(testFn);
548
+ blocks ? test.to.throw('SecurityException') : test.not.to.throw();
549
+
550
+ const nosqliResults = sourceContext.resultsMap['nosql-injection-mongo'];
551
+ const ssjsResult = sourceContext.resultsMap['ssjs-injection'];
552
+ if (type == 'expansionInjection') {
553
+ expect(nosqliResults[0].exploitMetadata).to.deep.equal(expectedDetails);
554
+ } else {
555
+ expect(ssjsResult[0].exploitMetadata).to.deep.equal(expectedDetails);
556
+ }
557
+ });
558
+ });
559
+
560
+ [
561
+ {
562
+ userInput: 'function() ',
563
+ cmd: 'function() { return true; }',
564
+ start: 0,
565
+ end: 10,
566
+ },
567
+ {
568
+ userInput: '(function() { return true })()',
569
+ cmd: 'function() { return (function() { return true })(); }',
570
+ start: 20,
571
+ end: 49,
572
+ }
573
+ ].forEach(({ userInput, cmd, start, end }, index) => {
574
+ it(`sourceContext-injection as function execution - case ${index + 1}`, function () {
575
+ const sinkContexts = [
576
+ {
577
+ value: { $where: cmd },
578
+ name: 'mongodb.query'
579
+ },
580
+ {
581
+ value: {
582
+ $expr: {
583
+ $function: {
584
+ body: cmd,
585
+ args: [],
586
+ lang: 'js',
587
+ }
588
+ }
589
+ },
590
+ name: 'mongodb.query'
591
+ },
592
+ ];
593
+
594
+ sinkContexts.forEach(sc => {
595
+ const data = {
596
+ sourceContext: makeSourceContext(
597
+ [
598
+ {
599
+ ruleId: 'ssjs-injection',
600
+ value: userInput,
601
+ idsList: [],
602
+ exploitMetadata: [],
603
+ }
604
+ ]
605
+ ),
606
+ };
607
+
608
+ handlers.nosqlInjectionMongo(data.sourceContext, sc);
609
+ expect(data.sourceContext.resultsMap['nosql-injection-mongo']).to.have.lengthOf(1);
610
+ expect(data.sourceContext.resultsMap['ssjs-injection']).to.have.lengthOf(1);
611
+ expect(data.sourceContext.resultsMap['nosql-injection-mongo'][0]).to.eql({
612
+ ruleId: 'nosql-injection-mongo',
613
+ value: userInput,
614
+ idsList: [],
615
+ exploitMetadata: [
616
+ {
617
+ sinkContext: sc,
618
+ findings: {
619
+ start,
620
+ end,
621
+ boundaryOverrunIndex: 0,
622
+ inputBoundaryIndex: 0
623
+ }
624
+ }
625
+ ],
626
+ mappedId: 'nosql-injection-mongo'
627
+ });
628
+ });
629
+ });
630
+ });
631
+
632
+ it('sourceContext-injection as function execution - $accumulator', function () {
633
+ const sinkContext = {
634
+ value: {
635
+ $group: {
636
+ _id: null,
637
+ count: {
638
+ $accumulator: {
639
+ init: 'function() { return { count: 0 } }',
640
+ accumulate: 'function(state) { return { count: state.count + 1 } }',
641
+ merge: 'function(state1, state2) { return { count: state1.count + state2.count } }',
642
+ finalize: 'function(state) { return state; }',
643
+ lang: 'js',
644
+ }
645
+ }
646
+ }
647
+ },
648
+ name: 'mongodb.query'
649
+ };
650
+
651
+ [
652
+ {
653
+ value: 'function() { return { count: 0 } }',
654
+ findings: {
655
+ boundaryOverrunIndex: 0,
656
+ end: 33,
657
+ inputBoundaryIndex: 0,
658
+ start: 0,
659
+ }
660
+ },
661
+ {
662
+ value: 'function(state) { return { count: state.count + 1 } }',
663
+ findings: {
664
+ boundaryOverrunIndex: 0,
665
+ end: 52,
666
+ inputBoundaryIndex: 0,
667
+ start: 0,
668
+ }
669
+ },
670
+ {
671
+ value: 'function(state1, state2) { return { count: state1.count + state2.count } }',
672
+ findings: {
673
+ boundaryOverrunIndex: 0,
674
+ end: 73,
675
+ inputBoundaryIndex: 0,
676
+ start: 0,
677
+ }
678
+ },
679
+ {
680
+ value: 'function(state) { return state; }',
681
+ findings: {
682
+ boundaryOverrunIndex: 0,
683
+ end: 32,
684
+ inputBoundaryIndex: 0,
685
+ start: 0,
686
+ }
687
+ },
688
+ ].forEach((obj) => {
689
+ [sinkContext, Object.assign({}, sinkContext, { value: [sinkContext.value] })]
690
+ .forEach(sc => {
691
+ const sourceContext = makeSourceContext(
692
+ [
693
+ {
694
+ ruleId: 'ssjs-injection',
695
+ value: obj.value,
696
+ exploitMetadata: [],
697
+ }
698
+ ]
699
+ );
700
+
701
+ handlers.nosqlInjectionMongo(sourceContext, sc);
702
+ expect(sourceContext.resultsMap['nosql-injection-mongo']).to.have.lengthOf(1);
703
+ expect(sourceContext.resultsMap['ssjs-injection']).to.have.lengthOf(1);
704
+ expect(sourceContext.resultsMap['nosql-injection-mongo'][0]).to.eql({
705
+ ruleId: 'nosql-injection-mongo',
706
+ value: obj.value,
707
+ exploitMetadata: [
708
+ {
709
+ sinkContext: sc,
710
+ findings: obj.findings,
711
+ }
712
+ ],
713
+ mappedId: 'nosql-injection-mongo'
714
+ });
715
+ });
716
+ });
717
+ });
718
+
719
+ it('sourceContext-injection as function execution - not an attack', function () {
720
+ const data = {
721
+ sourceContext: makeSourceContext(
722
+ [
723
+ {
724
+ ruleId: 'ssjs-injection',
725
+ value: ' ',
726
+ idsList: [],
727
+ exploitMetadata: [],
728
+ }
729
+ ]
730
+ ),
731
+ };
732
+
733
+ const sinkContext = {
734
+ value: { $where: 'function() { return true; } ' },
735
+ };
736
+
737
+ handlers.nosqlInjectionMongo(data.sourceContext, sinkContext);
738
+ expect(data.sourceContext.resultsMap['nosql-injection-mongo']).to.be.undefined;
739
+ expect(data.sourceContext.resultsMap['ssjs-injection']).to.have.lengthOf(1);
740
+ });
741
+
742
+ it(`nosql-injection-mongo - ${agentLibIDListTypes.TRUE_CLAUSE_1} - object`, function () {
743
+ const vector = "' || true || '";
744
+
745
+ const sinkContext = {
746
+ name: 'mongodb.FindCursor',
747
+ value: {
748
+ $where: `this.orderId === '${vector}'`
749
+ }
750
+ };
751
+
752
+ const expectedDetails = {
753
+ start: 18,
754
+ end: 31,
755
+ boundaryOverrunIndex: 0,
756
+ inputBoundaryIndex: 0,
757
+ };
758
+
759
+ const data = {
760
+ blocks: true,
761
+ type: 'expansionInjection',
762
+ findings: expansionFindings,
763
+ sourceContext: makeSourceContext(
764
+ [
765
+ {
766
+ ruleId: 'nosql-injection-mongo',
767
+ inputType: 'JsonValue',
768
+ path: [
769
+ 'input',
770
+ ],
771
+ key: 'input',
772
+ value: '\' || true || \'',
773
+ score: 10,
774
+ idsList: [
775
+ agentLibIDListTypes.TRUE_CLAUSE_1,
776
+ ],
777
+ mappedId: 'nosql-injection',
778
+ blocked: false,
779
+ exploitMetadata: [
780
+ ],
781
+ }
782
+ ],
783
+ {
784
+ ['nosql-injection-mongo']: 'block'
785
+ }
786
+ ),
787
+ expectedDetails: [
788
+ {
789
+ sinkContext: {
790
+ name: 'mongodb.FindCursor',
791
+ value: {
792
+ $where: `this.orderId === '${vector}'`,
793
+ }
794
+ },
795
+ findings: expansionFindings,
796
+ },
797
+ ]
798
+ };
799
+
800
+ const test = expect(() => handlers.nosqlInjectionMongo(data.sourceContext, sinkContext));
801
+ test.to.throw('SecurityException');
802
+
803
+ const nosqliResults = data.sourceContext.resultsMap['nosql-injection-mongo'];
804
+ expect(nosqliResults[0].exploitMetadata).to.deep.equal([
805
+ {
806
+ sinkContext: {
807
+ name: 'mongodb.FindCursor',
808
+ value: {
809
+ $where: `this.orderId === '${vector}'`,
810
+ },
811
+ stack: [],
812
+ },
813
+ findings: expectedDetails,
814
+ },
815
+ ]);
816
+ });
817
+
818
+ it(`nosql-injection-mongo - ${agentLibIDListTypes.MONGO_SLEEP} - object`, function () {
819
+ const sinkContext = {
820
+ name: 'mongodb.FindCursor',
821
+ value: {
822
+ $where: 'this.title ==sleep(500)',
823
+ }
824
+ };
825
+
826
+ const expectedDetails = {
827
+ start: 13,
828
+ end: 22,
829
+ boundaryOverrunIndex: 0,
830
+ inputBoundaryIndex: 0,
831
+ };
832
+
833
+ const data = {
834
+ blocks: true,
835
+ type: 'expansionInjection',
836
+ findings: expansionFindings,
837
+ sourceContext: makeSourceContext(
838
+ [
839
+ {
840
+ ruleId: 'nosql-injection-mongo',
841
+ inputType: 'JsonValue',
842
+ path: [
843
+ 'input',
844
+ ],
845
+ key: 'input',
846
+ value: 'sleep(500)',
847
+ score: 10,
848
+ idsList: [
849
+ agentLibIDListTypes.MONGO_SLEEP,
850
+ ],
851
+ mappedId: 'nosql-injection',
852
+ blocked: false,
853
+ exploitMetadata: [
854
+ ],
855
+ }
856
+ ],
857
+ {
858
+ ['nosql-injection-mongo']: 'block'
859
+ }
860
+ ),
861
+ expectedDetails: [
862
+ {
863
+ sinkContext: {
864
+ name: 'mongodb.FindCursor',
865
+ value: {
866
+ $where: 'this.title ==sleep(500)',
867
+ }
868
+ },
869
+ findings: expansionFindings,
870
+ },
871
+ ]
872
+ };
873
+
874
+ const test = expect(() => handlers.nosqlInjectionMongo(data.sourceContext, sinkContext));
875
+ test.to.throw('SecurityException');
876
+
877
+ const nosqliResults = data.sourceContext.resultsMap['nosql-injection-mongo'];
878
+ expect(nosqliResults[0].exploitMetadata).to.deep.equal([
879
+ {
880
+ sinkContext: {
881
+ name: 'mongodb.FindCursor',
882
+ value: {
883
+ $where: 'this.title ==sleep(500)',
884
+ },
885
+ stack: [
886
+ ],
887
+ },
888
+ findings: expectedDetails,
889
+ },
890
+ ]);
891
+ });
892
+
893
+ it(`nosql-injection-mongo - ${agentLibIDListTypes.MONGO_SLEEP} - string`, function () {
894
+ const sinkContext = {
895
+ name: 'mongodb.FindCursor',
896
+ value: 'sleep(500)'
897
+ };
898
+
899
+ const expectedDetails = {
900
+ start: 0,
901
+ end: 9,
902
+ boundaryOverrunIndex: 0,
903
+ inputBoundaryIndex: 0,
904
+ };
905
+
906
+ const data = {
907
+ blocks: true,
908
+ type: 'expansionInjection',
909
+ findings: expansionFindings,
910
+ sourceContext: makeSourceContext(
911
+ [
912
+ {
913
+ ruleId: 'nosql-injection-mongo',
914
+ inputType: 'JsonValue',
915
+ path: [
916
+ 'input',
917
+ ],
918
+ key: 'input',
919
+ value: 'sleep(500)',
920
+ score: 10,
921
+ idsList: [
922
+ agentLibIDListTypes.MONGO_SLEEP,
923
+ ],
924
+ mappedId: 'nosql-injection',
925
+ blocked: false,
926
+ exploitMetadata: [
927
+ ],
928
+ }
929
+ ],
930
+ {
931
+ ['nosql-injection-mongo']: 'block'
932
+ }
933
+ ),
934
+ expectedDetails: [
935
+ {
936
+ sinkContext: {
937
+ name: 'mongodb.FindCursor',
938
+ value: 'sleep(500)'
939
+ },
940
+ findings: expansionFindings,
941
+ },
942
+ ]
943
+ };
944
+
945
+ const test = expect(() => handlers.nosqlInjectionMongo(data.sourceContext, sinkContext));
946
+ test.to.throw('SecurityException');
947
+
948
+ const nosqliResults = data.sourceContext.resultsMap['nosql-injection-mongo'];
949
+ expect(nosqliResults[0].exploitMetadata).to.deep.equal([
950
+ {
951
+ sinkContext: {
952
+ name: 'mongodb.FindCursor',
953
+ value: 'sleep(500)',
954
+ stack: [],
955
+ },
956
+ findings: expectedDetails,
957
+ },
958
+ ]);
959
+ });
960
+ });
961
+
962
+ describe('ssjsInjection()', function () {
963
+ const stringSinkContextPositive = {
964
+ name: 'vm.Script',
965
+ value: 'function() { console.log("function() { process.exit(1); }") (function() { process.exit(1); })() }',
966
+ stack: [],
967
+ };
968
+ const stringSinkContextPositiveV2 = {
969
+ name: 'vm.Script',
970
+ value: 'function wrapperFunction() { function() { process.exit(1); } }',
971
+ stack: [],
972
+ };
973
+ const stringSinkContextNegative = {
974
+ name: 'vm.Script',
975
+ value: 'function() { console.log("function() { process.exit(1); }") }',
976
+ stack: [],
977
+ };
978
+ const objectSinkContextPositive = {
979
+ name: 'vm.Script.runInNewContext',
980
+ value: {
981
+ numValue: 1,
982
+ strValue: 'Contrast',
983
+ vulnerableValue: 'function() { process.exit(1); }',
984
+ anotherStr: 'Security',
985
+ booleanValue: true
986
+ },
987
+ stack: [],
988
+ };
989
+ const objectSinkContextNegative = {
990
+ name: 'vm.Script.runInNewContext',
991
+ value: {
992
+ numValue: 1,
993
+ strValue: '',
994
+ booleanValue: true
995
+ },
996
+ stack: [],
997
+ };
998
+ const findings = {
999
+ startIndex: 61,
1000
+ endIndex: 92,
1001
+ boundaryIndex: 61,
1002
+ codeString: 'function() { process.exit(1); }'
1003
+ };
1004
+
1005
+ const objectFindings = {
1006
+ startIndex: 0,
1007
+ endIndex: 30,
1008
+ boundaryIndex: 0,
1009
+ codeString: 'function() { process.exit(1); }'
1010
+ };
1011
+
1012
+ [
1013
+ {
1014
+ blocks: false,
1015
+ type: 'codeString',
1016
+ findings,
1017
+ sourceContext: makeSourceContext(
1018
+ [
1019
+ {
1020
+ ruleId: 'ssjs-injection',
1021
+ value: 'function() { process.exit(1); }',
1022
+ exploitMetadata: [],
1023
+ }
1024
+ ]
1025
+ ),
1026
+ expectedDetails: [
1027
+ {
1028
+ sinkContext: stringSinkContextPositive,
1029
+ findings,
1030
+ },
1031
+ {
1032
+ sinkContext: stringSinkContextPositiveV2,
1033
+ findings: {
1034
+ startIndex: 29,
1035
+ boundaryIndex: 29,
1036
+ endIndex: 60,
1037
+ codeString: 'function() { process.exit(1); }'
1038
+ },
1039
+ }
1040
+ ]
1041
+ },
1042
+ {
1043
+ blocks: true,
1044
+ type: 'codeString',
1045
+ findings,
1046
+ sourceContext: makeSourceContext(
1047
+ [
1048
+ {
1049
+ ruleId: 'ssjs-injection',
1050
+ value: 'function() { process.exit(1); }',
1051
+ exploitMetadata: [],
1052
+ }
1053
+ ],
1054
+ {
1055
+ ['ssjs-injection']: 'block'
1056
+ }
1057
+ ),
1058
+ expectedDetails: [
1059
+ {
1060
+ sinkContext: stringSinkContextPositive,
1061
+ findings,
1062
+ },
1063
+ ]
1064
+ },
1065
+ {
1066
+ blocks: false,
1067
+ type: 'codeString',
1068
+ findings,
1069
+ sourceContext: makeSourceContext(
1070
+ [
1071
+ {
1072
+ ruleId: 'ssjs-injection',
1073
+ value: 'function() { process.exit(1); }',
1074
+ exploitMetadata: [],
1075
+ }
1076
+ ],
1077
+ {
1078
+ ['ssjs-injection']: 'off'
1079
+ }
1080
+ ),
1081
+ expectedDetails: []
1082
+ },
1083
+ {
1084
+ blocks: false,
1085
+ type: 'contextObject',
1086
+ objectFindings,
1087
+ sourceContext: makeSourceContext(
1088
+ [
1089
+ {
1090
+ ruleId: 'ssjs-injection',
1091
+ value: 'function() { process.exit(1); }',
1092
+ exploitMetadata: [],
1093
+ }
1094
+ ]
1095
+ ),
1096
+ expectedDetails: [
1097
+ {
1098
+ sinkContext: objectSinkContextPositive,
1099
+ findings: objectFindings,
1100
+ },
1101
+ {
1102
+ sinkContext: objectSinkContextPositive,
1103
+ findings: objectFindings,
1104
+ }
1105
+ ]
1106
+ },
1107
+ {
1108
+ blocks: true,
1109
+ type: 'contextObject',
1110
+ objectFindings,
1111
+ sourceContext: makeSourceContext(
1112
+ [
1113
+ {
1114
+ ruleId: 'ssjs-injection',
1115
+ value: 'function() { process.exit(1); }',
1116
+ exploitMetadata: [],
1117
+ }
1118
+ ],
1119
+ {
1120
+ ['ssjs-injection']: 'block'
1121
+ }
1122
+ ),
1123
+ expectedDetails: [
1124
+ {
1125
+ sinkContext: objectSinkContextPositive,
1126
+ findings: objectFindings,
1127
+ },
1128
+ ]
1129
+ },
1130
+ {
1131
+ blocks: false,
1132
+ type: 'contextObject',
1133
+ objectFindings,
1134
+ sourceContext: makeSourceContext(
1135
+ [
1136
+ {
1137
+ ruleId: 'ssjs-injection',
1138
+ value: 'function() { process.exit(1); }',
1139
+ exploitMetadata: [],
1140
+ }
1141
+ ],
1142
+ {
1143
+ ['ssjs-injection']: 'off'
1144
+ }
1145
+ ),
1146
+ expectedDetails: []
1147
+ }
1148
+ ].forEach(({ blocks, type, sourceContext, expectedDetails }) => {
1149
+ it(makeTestDescription(blocks), function () {
1150
+ const testFn = () => {
1151
+ if (type == 'codeString') {
1152
+ handlers.ssjsInjection(sourceContext, stringSinkContextPositive);
1153
+ handlers.ssjsInjection(sourceContext, stringSinkContextPositiveV2);
1154
+ handlers.ssjsInjection(sourceContext, stringSinkContextNegative);
1155
+ } else {
1156
+ handlers.ssjsInjection(sourceContext, objectSinkContextPositive);
1157
+ handlers.ssjsInjection(sourceContext, objectSinkContextPositive);
1158
+ handlers.ssjsInjection(sourceContext, objectSinkContextNegative);
1159
+ }
1160
+ };
1161
+
1162
+ const test = expect(testFn);
1163
+ blocks ? test.to.throw('SecurityException') : test.not.to.throw();
1164
+
1165
+ const ssjsiResults = sourceContext.resultsMap['ssjs-injection'];
1166
+ expect(ssjsiResults[0].exploitMetadata).to.deep.equal(expectedDetails);
1167
+ });
1168
+ });
1169
+ });
1170
+
1171
+ describe('handleReflectedXss()', function () {
1172
+ const ruleId = 'reflected-xss';
1173
+ const value = '" onmouseover="alert(3)"';
1174
+ const sinkContextPositive = {
1175
+ value,
1176
+ stack: []
1177
+ };
1178
+ const sinkContextNegative = {
1179
+ value: 'foo',
1180
+ stack: []
1181
+ };
1182
+ const findings = {
1183
+ value
1184
+ };
1185
+
1186
+ [
1187
+ {
1188
+ blocks: false,
1189
+ sourceContext: makeSourceContext(
1190
+ [{
1191
+ ruleId,
1192
+ value,
1193
+ exploitMetadata: []
1194
+ }]
1195
+ ),
1196
+ expectedDetails: [
1197
+ {
1198
+ sinkContext: sinkContextPositive,
1199
+ findings
1200
+ },
1201
+ {
1202
+ sinkContext: sinkContextPositive,
1203
+ findings
1204
+ }
1205
+ ]
1206
+ },
1207
+ {
1208
+ blocks: false,
1209
+ sourceContext: makeSourceContext(
1210
+ [{
1211
+ ruleId,
1212
+ value,
1213
+ exploitMetadata: [],
1214
+ }],
1215
+ {
1216
+ [ruleId]: 'off'
1217
+ }
1218
+ ),
1219
+ expectedDetails: []
1220
+ }
1221
+ ].forEach(({ blocks, sourceContext, expectedDetails }) => {
1222
+ it(makeTestDescription(blocks), function () {
1223
+ const testFn = () => {
1224
+ handlers.handleReflectedXss(sourceContext, sinkContextPositive);
1225
+ handlers.handleReflectedXss(sourceContext, sinkContextPositive);
1226
+ handlers.handleReflectedXss(sourceContext, sinkContextNegative);
1227
+ };
1228
+
1229
+ const test = expect(testFn);
1230
+ blocks ? test.to.throw('SecurityException') : test.not.to.throw();
1231
+
1232
+ expect(sourceContext.resultsMap['reflected-xss'][0].exploitMetadata).to.deep.equal(expectedDetails);
1233
+ });
1234
+ });
1235
+ });
1236
+ });