@contrast/protect 1.0.1

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 (48) hide show
  1. package/LICENSE +12 -0
  2. package/README.md +9 -0
  3. package/lib/cli-rewriter.js +20 -0
  4. package/lib/error-handlers/constants.js +5 -0
  5. package/lib/error-handlers/index.js +13 -0
  6. package/lib/error-handlers/install/fastify3.js +88 -0
  7. package/lib/error-handlers/install/fastify3.test.js +142 -0
  8. package/lib/esm-loader.mjs +2 -0
  9. package/lib/esm-loader.test.mjs +11 -0
  10. package/lib/index.d.ts +36 -0
  11. package/lib/index.js +89 -0
  12. package/lib/index.test.js +32 -0
  13. package/lib/input-analysis/handlers.js +462 -0
  14. package/lib/input-analysis/handlers.test.js +898 -0
  15. package/lib/input-analysis/index.js +16 -0
  16. package/lib/input-analysis/index.test.js +28 -0
  17. package/lib/input-analysis/install/fastify3.js +79 -0
  18. package/lib/input-analysis/install/fastify3.test.js +71 -0
  19. package/lib/input-analysis/install/http.js +185 -0
  20. package/lib/input-analysis/install/http.test.js +315 -0
  21. package/lib/input-tracing/constants.js +5 -0
  22. package/lib/input-tracing/handlers/index.js +117 -0
  23. package/lib/input-tracing/handlers/index.test.js +395 -0
  24. package/lib/input-tracing/handlers/nosql-injection-mongo.js +48 -0
  25. package/lib/input-tracing/index.js +32 -0
  26. package/lib/input-tracing/install/README.md +1 -0
  27. package/lib/input-tracing/install/child-process.js +45 -0
  28. package/lib/input-tracing/install/child-process.test.js +112 -0
  29. package/lib/input-tracing/install/fs.js +107 -0
  30. package/lib/input-tracing/install/fs.test.js +118 -0
  31. package/lib/input-tracing/install/mysql.js +57 -0
  32. package/lib/input-tracing/install/mysql.test.js +108 -0
  33. package/lib/input-tracing/install/postgres.js +61 -0
  34. package/lib/input-tracing/install/postgres.test.js +125 -0
  35. package/lib/input-tracing/install/sequelize.js +51 -0
  36. package/lib/input-tracing/install/sequelize.test.js +79 -0
  37. package/lib/input-tracing/install/sqlite3.js +45 -0
  38. package/lib/input-tracing/install/sqlite3.test.js +88 -0
  39. package/lib/make-response-blocker.js +35 -0
  40. package/lib/make-response-blocker.test.js +88 -0
  41. package/lib/make-source-context.js +130 -0
  42. package/lib/make-source-context.test.js +298 -0
  43. package/lib/security-exception.js +12 -0
  44. package/lib/throw-security-exception.js +30 -0
  45. package/lib/throw-security-exception.test.js +50 -0
  46. package/lib/utils.js +88 -0
  47. package/lib/utils.test.js +40 -0
  48. package/package.json +32 -0
@@ -0,0 +1,898 @@
1
+ 'use strict';
2
+
3
+ const { expect } = require('chai');
4
+ const sinon = require('sinon');
5
+ const { constants: { RuleType, InputType } } = require('@contrast/agent-lib');
6
+ const sqlMask = RuleType['sql-injection'];
7
+ const xssMask = RuleType['reflected-xss'];
8
+ const mongoMask = RuleType['nosql-injection-mongo'];
9
+ const preferWW = { preferWorthWatching: true };
10
+ const { UrlParameter, ParameterKey, ParameterValue } = InputType;
11
+ /* eslint-disable newline-per-chained-call */
12
+
13
+ // FASTIFY tests (NODE-2239, NODE-2240, NODE-2241)
14
+ describe('basic input-analysis handlers', function () {
15
+ let mocks,
16
+ core,
17
+ sourceContext,
18
+ handleConnect,
19
+ handleRequestEnd,
20
+ handleQueryParams,
21
+ handleUrlParams,
22
+ handleCookies,
23
+ handleParsedBody,
24
+ handleFileUploadName;
25
+
26
+ beforeEach(function () {
27
+ mocks = require('../../../test/mocks');
28
+ core = {
29
+ config: mocks.config(),
30
+ protect: mocks.protect(),
31
+ logger: {
32
+ debug: sinon.stub(),
33
+ }
34
+ };
35
+ require('./handlers')(core);
36
+ ({
37
+ protect: {
38
+ inputAnalysis: {
39
+ handleConnect,
40
+ handleRequestEnd,
41
+ handleQueryParams,
42
+ handleUrlParams,
43
+ handleCookies,
44
+ handleParsedBody,
45
+ handleFileUploadName,
46
+ }
47
+ }
48
+ } = core);
49
+
50
+ sourceContext = {
51
+ rules: {
52
+ agentLibRulesMask: sqlMask,
53
+ agentLibRules: {
54
+ 'sql-injection': { mode: 'block' },
55
+ },
56
+ },
57
+ block: sinon.stub(),
58
+ findings: {
59
+ trackRequest: false,
60
+ securityException: undefined,
61
+ bodyType: undefined,
62
+ resultsMap: Object.create(null),
63
+ },
64
+ reqData: {
65
+ method: 'GET',
66
+ uriPath: '/',
67
+ search: '',
68
+ headers: { 'content-type': 'application/json' },
69
+ contentType: '',
70
+ }
71
+ };
72
+ });
73
+
74
+
75
+ describe('handleConnect', function () {
76
+ let connectInputs;
77
+
78
+ beforeEach(function () {
79
+ connectInputs = {
80
+ headers: ['content-type', '.25 beretta', 'user-agent', '; drop table *'],
81
+ };
82
+ sinon.spy(core.protect.agentLib, 'scoreRequestConnect');
83
+ });
84
+
85
+ it('does not block a request with an attack for a worth-watching rule', function () {
86
+ handleConnect(sourceContext, connectInputs);
87
+
88
+ const expectedReturnValue = {
89
+ trackRequest: true,
90
+ resultsList: [{
91
+ ruleId: 'sql-injection',
92
+ inputType: 'HeaderValue',
93
+ path: ['user-agent'],
94
+ key: 'user-agent',
95
+ value: '; drop table *',
96
+ score: 10,
97
+ idsList: [],
98
+ // added by "normalizeFindings"
99
+ details: [],
100
+ blocked: false,
101
+ mappedId: 'sql-injection',
102
+ }],
103
+ };
104
+ const { agentLib } = core.protect;
105
+ expect(agentLib.scoreRequestConnect).calledOnceWith(sqlMask, connectInputs, preferWW);
106
+ // i don't know why i can't use expect(core.protect...scoreRequestConnect).returned(expected...);
107
+ const rv = agentLib.scoreRequestConnect.getCall(0).returnValue;
108
+ expect(rv).eql(expectedReturnValue);
109
+
110
+ // it calls with worth watching, so only reflected-xss can return a 90
111
+ expect(sourceContext.block).callCount(0);
112
+
113
+ });
114
+
115
+ // non-worth-watching rules
116
+ ['path-traversal', 'reflected-xss', 'method-tampering'].forEach((rule) => {
117
+ it(`blocks an attack for non-worth-watching rule: ${rule}`, function () {
118
+ const { agentLib } = core.protect;
119
+ sourceContext.rules = {
120
+ agentLibRulesMask: agentLib.RuleType[rule],
121
+ agentLibRules: { [rule]: { mode: 'block' } }
122
+ };
123
+ const target = {
124
+ 'path-traversal': 'queries',
125
+ 'reflected-xss': 'queries',
126
+ 'method-tampering': 'method',
127
+ };
128
+ const attack = {
129
+ 'path-traversal': ['x', '../..'],
130
+ 'reflected-xss': ['x', '<script'],
131
+ 'method-tampering': 'XYZZY',
132
+ };
133
+ connectInputs[target[rule]] = attack[rule];
134
+
135
+ const block = handleConnect(sourceContext, connectInputs);
136
+
137
+ expect(agentLib.scoreRequestConnect).calledOnceWith(agentLib.RuleType[rule], connectInputs, preferWW);
138
+ expect(sourceContext.block).callCount(0);
139
+ expect(block).eql(['block', rule]);
140
+ });
141
+ });
142
+ });
143
+
144
+ describe('handleRequestEnd', function() {
145
+ it('nyi', function() {
146
+ expect(() => handleRequestEnd()).to.throw('nyi');
147
+ });
148
+ });
149
+
150
+ describe('handleQueryParams', function() {
151
+ beforeEach(function() {
152
+ sinon.spy(core.protect.agentLib, 'scoreAtom');
153
+ sinon.spy(core.protect.agentLib, 'getMongoQueryType');
154
+ });
155
+
156
+ it('does not block a request with an attack for a worth-watching rule', function() {
157
+ const queryParams = {
158
+ first: { a: { nested: '; drop table *' } },
159
+ second: '',
160
+ third: 'times-a-charm',
161
+ };
162
+ const expectedCalls = [
163
+ { type: ParameterKey, value: 'first', rv: undefined },
164
+ { type: ParameterKey, value: 'a', rv: undefined },
165
+ { type: ParameterKey, value: 'nested', rv: undefined },
166
+ { type: ParameterValue, value: '; drop table *', rv: [{ ruleId: 'sql-injection', score: 10 }] },
167
+ { type: ParameterKey, value: 'second', rv: undefined },
168
+ { type: ParameterKey, value: 'third', rv: undefined },
169
+ { type: ParameterValue, value: 'times-a-charm', rv: [{ ruleId: 'sql-injection', score: 10 }] },
170
+
171
+ ];
172
+ handleQueryParams(sourceContext, queryParams);
173
+
174
+ const { agentLib } = core.protect;
175
+ // note that the empty string for the key 'second' does not generate a callback in handleQueryParams.
176
+ expect(agentLib.scoreAtom).callCount(7);
177
+
178
+ for (let i = 0; i < expectedCalls.length; i++) {
179
+ const exp = expectedCalls[i];
180
+ const call = agentLib.scoreAtom.getCall(i);
181
+ expect(call).calledWith(sqlMask, exp.value, exp.type, preferWW);
182
+ expect(call.returnValue).eql(exp.rv);
183
+ }
184
+
185
+ // it calls with worth watching, so only reflected-xss can return a 90
186
+ expect(sourceContext.block).callCount(0);
187
+
188
+ });
189
+
190
+ it('blocks a request with a definite attack for a non-worth-watching rule', function() {
191
+ const xssMask = RuleType['reflected-xss'];
192
+ sourceContext.rules = {
193
+ agentLibRulesMask: xssMask,
194
+ agentLibRules: {
195
+ 'reflected-xss': { mode: 'block' },
196
+ },
197
+ };
198
+ const queryParams = {
199
+ first: { a: { nested: '<script' } },
200
+ second: '',
201
+ third: 'eval(x.toString())',
202
+ };
203
+
204
+ const expectedCalls = [
205
+ { type: ParameterKey, value: 'first', rv: undefined },
206
+ { type: ParameterKey, value: 'a', rv: undefined },
207
+ { type: ParameterKey, value: 'nested', rv: undefined },
208
+ { type: ParameterValue, value: '<script', rv: [{ ruleId: 'reflected-xss', score: 90 }] },
209
+ { type: ParameterKey, value: 'second', rv: undefined },
210
+ { type: ParameterKey, value: 'third', rv: undefined },
211
+ { type: ParameterValue, value: 'eval(x.toString())', rv: [{ ruleId: 'reflected-xss', score: 90 }] },
212
+
213
+ ];
214
+ handleQueryParams(sourceContext, queryParams);
215
+
216
+ const { agentLib } = core.protect;
217
+ // note that the empty string for the key 'second' does not generate a callback in handleQueryParams.
218
+ expect(agentLib.scoreAtom).callCount(7);
219
+
220
+ for (let i = 0; i < expectedCalls.length; i++) {
221
+ const exp = expectedCalls[i];
222
+ const call = agentLib.scoreAtom.getCall(i);
223
+ expect(call).calledWith(xssMask, exp.value, exp.type, preferWW);
224
+ expect(call.returnValue).eql(exp.rv);
225
+ }
226
+
227
+ // scoreAtom is called with worth watching, so only reflected-xss can return a 90
228
+ expect(sourceContext.block).callCount(1);
229
+ });
230
+
231
+ it('handles nosql-injection-mongo', function() {
232
+ const queryParams = {
233
+ first: { user: { $eq: { id: '*' } } }
234
+ };
235
+ sourceContext.rules = {
236
+ agentLibRulesMask: mongoMask,
237
+ agentLibRules: {
238
+ 'nosql-injection-mongo': { mode: 'block' },
239
+ },
240
+ };
241
+ const { agentLib } = core.protect;
242
+
243
+ handleQueryParams(sourceContext, queryParams);
244
+
245
+ expect(agentLib.scoreAtom).callCount(5);
246
+ expect(agentLib.getMongoQueryType).callCount(4);
247
+
248
+ expect(sourceContext.findings.trackRequest).equal(true);
249
+ expect(sourceContext.findings.securityException).equal(undefined);
250
+ expect(sourceContext.findings.resultsMap).an('object').keys('nosql-injection-mongo');
251
+
252
+ const results = sourceContext.findings.resultsMap['nosql-injection-mongo'];
253
+ expect(results).an('array').length(1);
254
+
255
+ const result = results[0];
256
+ expect(result.path).eql(['first', 'user']);
257
+ expect(result.inputType).equal('ParameterKey');
258
+ expect(result.key).equal('$eq');
259
+ expect(result.value).equal(agentLib.getMongoQueryType('$eq'));
260
+ expect(result.mongoContext).an('object').keys('inputToCheck');
261
+
262
+ const context = result.mongoContext;
263
+ expect(context.inputToCheck).eql({ id: '*' });
264
+ });
265
+
266
+ it('handles nested nosql-injection-mongo results', function() {
267
+ const queryParams = {
268
+ first: { user: { $eq: { id: { $ne: { password: '' } } } } }
269
+ };
270
+ sourceContext.rules = {
271
+ agentLibRulesMask: mongoMask,
272
+ agentLibRules: {
273
+ 'nosql-injection-mongo': { mode: 'block' },
274
+ },
275
+ };
276
+ const { agentLib } = core.protect;
277
+
278
+ handleQueryParams(sourceContext, queryParams);
279
+
280
+ expect(agentLib.scoreAtom).callCount(6);
281
+ expect(agentLib.getMongoQueryType).callCount(6);
282
+
283
+ expect(sourceContext.findings.trackRequest).equal(true);
284
+ expect(sourceContext.findings.securityException).equal(undefined);
285
+ expect(sourceContext.findings.resultsMap).an('object').keys('nosql-injection-mongo');
286
+
287
+ const results = sourceContext.findings.resultsMap['nosql-injection-mongo'];
288
+ expect(results).an('array').length(2);
289
+
290
+ const exp = [{
291
+ path: ['first', 'user'],
292
+ key: '$eq',
293
+ inputToCheck: { id: { $ne: { password: '' } } },
294
+ }, {
295
+ path: ['first', 'user', '$eq', 'id'],
296
+ key: '$ne',
297
+ inputToCheck: { password: '' },
298
+ }];
299
+
300
+ for (let i = 0; i < exp.length; i++) {
301
+ expect(results[i].path).eql(exp[i].path);
302
+ expect(results[i].inputType).equal('ParameterKey');
303
+ expect(results[i].key).equal(exp[i].key);
304
+ expect(results[i].value).equal(agentLib.getMongoQueryType(exp[i].key));
305
+ expect(results[i].mongoContext).an('object').keys('inputToCheck');
306
+
307
+ expect(results[i].mongoContext.inputToCheck).eql(exp[i].inputToCheck);
308
+ }
309
+ });
310
+
311
+ it('handles multiple nosql-injection-mongo results', function() {
312
+ const queryParams = {
313
+ first: {
314
+ user: { $where: { id: '*' } },
315
+ password: { $finalize: 'while(1){};' },
316
+ }
317
+ };
318
+ sourceContext.rules = {
319
+ agentLibRulesMask: mongoMask,
320
+ agentLibRules: {
321
+ 'nosql-injection-mongo': { mode: 'block' },
322
+ },
323
+ };
324
+ const { agentLib } = core.protect;
325
+
326
+ handleQueryParams(sourceContext, queryParams);
327
+
328
+ expect(agentLib.scoreAtom).callCount(8);
329
+ expect(agentLib.getMongoQueryType).callCount(6);
330
+
331
+ expect(sourceContext.findings.trackRequest).equal(true, 'trackRequest should be true');
332
+ expect(sourceContext.findings.securityException).equal(undefined);
333
+ expect(sourceContext.findings.resultsMap).an('object').keys('nosql-injection-mongo');
334
+
335
+ const results = sourceContext.findings.resultsMap['nosql-injection-mongo'];
336
+ expect(results).an('array').length(2);
337
+
338
+ const exp = [{
339
+ path: ['first', 'user'],
340
+ key: '$where',
341
+ inputToCheck: { id: '*' },
342
+ }, {
343
+ path: ['first', 'password'],
344
+ key: '$finalize',
345
+ inputToCheck: 'while(1){};',
346
+ }];
347
+
348
+ for (let i = 0; i < exp.length; i++) {
349
+ expect(results[i].path).eql(exp[i].path);
350
+ expect(results[i].inputType).equal('ParameterKey');
351
+ expect(results[i].key).equal(exp[i].key);
352
+ expect(results[i].value).equal(agentLib.getMongoQueryType(exp[i].key));
353
+ expect(results[i].mongoContext).an('object').keys('inputToCheck');
354
+
355
+ expect(results[i].mongoContext.inputToCheck).eql(exp[i].inputToCheck);
356
+ }
357
+ });
358
+ });
359
+
360
+ describe('handleUrlParams', function () {
361
+ beforeEach(function () {
362
+ sinon.spy(core.protect.agentLib, 'scoreAtom');
363
+ });
364
+
365
+ it('does not block a request with an attack for a worth-watching rule', function() {
366
+ const urlParams = {
367
+ first: { a: { nested: '; drop table *' } },
368
+ second: '',
369
+ third: 'times-a-charm',
370
+ };
371
+ handleUrlParams(sourceContext, urlParams);
372
+
373
+ const { agentLib } = core.protect;
374
+ // scoreAtom is only called from leaf, not key, values that are not falsey.
375
+ expect(agentLib.scoreAtom).callCount(2);
376
+ expect(agentLib.scoreAtom.firstCall).calledWith(sqlMask, '; drop table *', UrlParameter, preferWW);
377
+ expect(agentLib.scoreAtom.secondCall).calledWith(sqlMask, 'times-a-charm', UrlParameter, preferWW);
378
+
379
+ for (let i = 0; i < 2; i++) {
380
+ const rv = agentLib.scoreAtom.getCall(i).returnValue;
381
+ expect(rv).eql([{ ruleId: 'sql-injection', score: 10 }]);
382
+ }
383
+
384
+ // it calls with worth watching, so only reflected-xss can return a 90
385
+ expect(sourceContext.block).callCount(0);
386
+
387
+ });
388
+
389
+ it('blocks a request with a definite attack for a non-worth-watching rule', function() {
390
+ const xssMask = RuleType['reflected-xss'];
391
+ sourceContext.rules = {
392
+ agentLibRulesMask: xssMask,
393
+ agentLibRules: {
394
+ 'reflected-xss': { mode: 'block' },
395
+ },
396
+ };
397
+ const urlParams = {
398
+ first: { a: { nested: '<script' } },
399
+ second: '',
400
+ third: 'eval(x.toString())',
401
+ };
402
+
403
+ handleUrlParams(sourceContext, urlParams);
404
+
405
+ const { agentLib } = core.protect;
406
+ // scoreAtom is only called from leaf, not key, values that are not falsey.
407
+ expect(agentLib.scoreAtom).callCount(2);
408
+ expect(agentLib.scoreAtom.firstCall).calledWith(xssMask, '<script', UrlParameter, preferWW);
409
+ expect(agentLib.scoreAtom.secondCall).calledWith(xssMask, 'eval(x.toString())', UrlParameter, preferWW);
410
+
411
+ for (let i = 0; i < 2; i++) {
412
+ const rv = agentLib.scoreAtom.getCall(i).returnValue;
413
+ expect(rv).eql([{ ruleId: 'reflected-xss', score: 90 }]);
414
+ }
415
+
416
+ // it calls with worth watching, so only reflected-xss can return a 90
417
+ expect(sourceContext.block).callCount(1);
418
+ });
419
+
420
+ // there are no longer any worth-watching returns from a non-worth-watching rule. there
421
+ // used to be, but there are now. this test is skipped (as opposed to removed) to serve
422
+ // as a guide in case this can happen in the future.
423
+ it.skip('does not block a request for a worth-watching score from a non-worth-watching rule', function() {
424
+ sourceContext.rules = {
425
+ agentLibRulesMask: xssMask,
426
+ agentLibRules: {
427
+ 'reflected-xss': { mode: 'block' },
428
+ },
429
+ };
430
+ // this is the only Score::HIGH pattern left; if it goes away then this
431
+ // test should go away too.
432
+ const tenScore1 = 'type="text/javascript"';
433
+ const tenScore2 = 'type="application/javascript"';
434
+ const urlParams = {
435
+ first: { a: { nested: tenScore1 } },
436
+ second: '',
437
+ third: tenScore2,
438
+ };
439
+
440
+ handleUrlParams(sourceContext, urlParams);
441
+
442
+ const { agentLib } = core.protect;
443
+ // scoreAtom is only called from leaf, not key, values that are not falsey.
444
+ expect(agentLib.scoreAtom).callCount(2);
445
+ expect(agentLib.scoreAtom.firstCall).calledWith(xssMask, tenScore1, UrlParameter, preferWW);
446
+ expect(agentLib.scoreAtom.secondCall).calledWith(xssMask, tenScore2, UrlParameter, preferWW);
447
+
448
+ for (let i = 0; i < 2; i++) {
449
+ const rv = agentLib.scoreAtom.getCall(i).returnValue;
450
+ expect(rv).eql([{ ruleId: 'reflected-xss', score: 10 }]);
451
+ }
452
+
453
+ // while reflected-xss can return a 90 score, these particular attacks only
454
+ // score 10, so block shouldn't be called.
455
+ expect(sourceContext.block).callCount(0);
456
+ expect(sourceContext.findings.resultsMap).an('object').keys(['reflected-xss']);
457
+ const expected = {
458
+ path: [['first', 'a', 'nested'], ['third']],
459
+ key: ['nested', 'third'],
460
+ value: [tenScore1, tenScore2]
461
+ };
462
+ expect(sourceContext.findings.resultsMap).an('object').keys(['reflected-xss']);
463
+ const reflectedXssResults = sourceContext.findings.resultsMap['reflected-xss'];
464
+
465
+ for (let i = 0; i < reflectedXssResults.length; i++) {
466
+ const result = reflectedXssResults[i];
467
+ expect(result.ruleId).equal('reflected-xss');
468
+ expect(result.inputType).equal('UrlParameter');
469
+ expect(result.path).eql(expected.path[i]);
470
+ expect(result.key).equal(expected.key[i]);
471
+ expect(result.value).equal(expected.value[i]);
472
+ }
473
+ });
474
+ });
475
+
476
+ describe('handleCookies', function() {
477
+ const sql10Score = '; drop table *';
478
+ let al;
479
+
480
+ beforeEach(function () {
481
+ al = core.protect.agentLib;
482
+ });
483
+
484
+ it('does not block the request because worth-watching is used', function () {
485
+ const cookies = {
486
+ secret: sql10Score,
487
+ };
488
+
489
+ const expectedInputs = { cookies: [] };
490
+ Object.entries(cookies).forEach(entry => expectedInputs.cookies.push(...entry));
491
+ sinon.spy(al, 'scoreRequestConnect');
492
+
493
+ handleCookies(sourceContext, cookies);
494
+
495
+ expect(al.scoreRequestConnect).calledOnceWith(sqlMask, expectedInputs, preferWW);
496
+ expect(sourceContext.block).callCount(0);
497
+ });
498
+ });
499
+
500
+ describe('handleParsedBody', function() {
501
+ const sql10Score = '; drop table *';
502
+ const xss90Score = '<script';
503
+ let al;
504
+ let sc;
505
+
506
+ beforeEach(function () {
507
+ al = core.protect.agentLib;
508
+ sc = sourceContext;
509
+ sinon.spy(al, 'scoreAtom');
510
+ });
511
+
512
+ it('blocks the request when a 90 score for reflected-xss', function () {
513
+ sc.rules.agentLibRulesMask |= xssMask;
514
+ sc.rules.agentLibRules['reflected-xss'] = { mode: 'block' };
515
+ const body = {
516
+ secret: sql10Score,
517
+ [xss90Score]: 'special',
518
+ };
519
+ const mask = sc.rules.agentLibRulesMask;
520
+
521
+ handleParsedBody(sourceContext, body);
522
+
523
+ const PK = al.InputType.ParameterKey;
524
+ const PV = al.InputType.ParameterValue;
525
+
526
+ expect(al.scoreAtom).callCount(4);
527
+ expect(al.scoreAtom.getCall(0).args).eql([mask, 'secret', PK, preferWW]);
528
+ expect(al.scoreAtom.getCall(1).args).eql([mask, sql10Score, PV, preferWW]);
529
+ expect(al.scoreAtom.getCall(2).args).eql([mask, xss90Score, PK, preferWW]);
530
+ expect(al.scoreAtom.getCall(3).args).eql([mask, 'special', PV, preferWW]);
531
+ expect(sourceContext.block).calledOnceWith('block', 'reflected-xss');
532
+
533
+ expect(sourceContext.findings.resultsMap).an('object').keys(['sql-injection', 'reflected-xss']);
534
+ const expected = [
535
+ { ruleId: 'sql-injection', inputType: 'ParameterValue', path: ['secret'], key: 'secret', value: sql10Score, score: 10 },
536
+ { ruleId: 'reflected-xss', inputType: 'ParameterKey', path: [], key: xss90Score, value: xss90Score, score: 90 },
537
+ ];
538
+
539
+ for (let i = 0; i < expected.length; i++) {
540
+ expect(sourceContext.findings.resultsMap[expected[i].ruleId]).an('array').length(1);
541
+
542
+ const result = sourceContext.findings.resultsMap[expected[i].ruleId][0];
543
+ expect(result.ruleId).equal(expected[i].ruleId);
544
+ expect(result.inputType).equal(expected[i].inputType);
545
+ expect(result.path).eql(expected[i].path);
546
+ expect(result.key).equal(expected[i].key);
547
+ expect(result.value).equal(expected[i].value);
548
+ expect(result.score).equal(expected[i].score);
549
+ }
550
+ });
551
+
552
+ it('does not block the request if the request is not tracked', function () {
553
+ const body = { secret: sql10Score };
554
+
555
+ handleParsedBody(sourceContext, body);
556
+
557
+ expect(al.scoreAtom).callCount(2);
558
+ expect(sourceContext.block).callCount(0);
559
+
560
+ expect(sourceContext.findings.resultsMap).an('object').keys(['sql-injection']);
561
+ const expected = {
562
+ ruleId: ['sql-injection'],
563
+ inputType: ['ParameterValue'],
564
+ path: [['secret']],
565
+ key: ['secret'],
566
+ value: [sql10Score],
567
+ score: [10],
568
+ };
569
+ for (let i = 0; i < expected.length; i++) {
570
+ expect(sourceContext.findings.resultsMap[expected[i].ruleId]).an('array').length(1);
571
+
572
+ const result = sourceContext.findings.resultsMap[expected[i].ruleId][0];
573
+ expect(result.ruleId).equal(expected[i].ruleId);
574
+ expect(result.inputType).equal(expected[i].inputType);
575
+ expect(result.path).eql(expected[i].path);
576
+ expect(result.key).equal(expected[i].key);
577
+ expect(result.value).equal(expected[i].value);
578
+ expect(result.score).equal(expected[i].score);
579
+ }
580
+ });
581
+ });
582
+
583
+ describe('handleFileUploadName', function() {
584
+ it('nyi', function() {
585
+ expect(() => handleFileUploadName()).to.throw('nyi');
586
+ });
587
+ });
588
+ });
589
+
590
+ // NODE-2170 tests
591
+ const EventEmitter = require('events');
592
+
593
+ const agentLib = require('@contrast/agent-lib');
594
+ const Protect = require('../../../protect');
595
+
596
+ const mocks = require('../../../test/mocks');
597
+
598
+ describe('INPUT ANALYSIS', function() {
599
+
600
+ describe('handlers with mock agent-lib', function() {
601
+ let core;
602
+ let inputAnalysis;
603
+ let server;
604
+ let serverEmit;
605
+ let req, res;
606
+ let reqEmitter;
607
+ let expectedReqData;
608
+ let connectInputs;
609
+ let scoreRequestConnect;
610
+ let srcReturnValue;
611
+
612
+ beforeEach(function() {
613
+ //console.log(this.currentTest.title);
614
+ core = mocks.core();
615
+ core.depHooks = mocks.depHooks();
616
+ core.config = mocks.config();
617
+ core.patcher = mocks.patcher();
618
+ Object.assign(core.config.protect.rules, {
619
+ 'cmd-injection': { mode: 'block' }
620
+ });
621
+ core.logger = mocks.logger();
622
+ core.scopes = mocks.scopes();
623
+
624
+ srcReturnValue = {
625
+ trackRequest: true,
626
+ resultsList: [{
627
+ score: 90.0,
628
+ ruleId: 'cmd-injection',
629
+ }],
630
+ };
631
+ scoreRequestConnect = sinon.stub().callsFake(() => srcReturnValue);
632
+
633
+ sinon.stub(Protect, 'instantiateAgentLib').returns({
634
+ // indirection so it can be changed in each test
635
+ scoreRequestConnect,
636
+ RuleType: agentLib.constants.RuleType,
637
+ InputType: agentLib.constants.InputType,
638
+ // MongoQueryType,
639
+ // DbType,
640
+ });
641
+
642
+ core.protect = Protect(core);
643
+
644
+ sinon.spy(core.protect.inputAnalysis, 'handleConnect');
645
+
646
+ inputAnalysis = require('./install/http')(core);
647
+ inputAnalysis.install();
648
+ sinon.spy(inputAnalysis, 'makeSourceContext');
649
+ sinon.spy(inputAnalysis.scope, 'getStore');
650
+
651
+ // mock Server thing...
652
+ server = {};
653
+ serverEmit = sinon.stub();
654
+ server.prototype = { emit: serverEmit };
655
+
656
+ // mock req, res
657
+ reqEmitter = new EventEmitter();
658
+ req = {
659
+ url: '/',
660
+ method: 'GET',
661
+ rawHeaders: ['host', 'vogon.com', 'content-type', 'text/json'],
662
+ // this needs to look sort of like a real incoming message
663
+ emit: (...args) => reqEmitter.emit(...args),
664
+ _events: {},
665
+ socket: { _readableState: { autoDestroy: false } },
666
+ resume: sinon.stub(),
667
+ };
668
+ res = {
669
+ end: sinon.stub(),
670
+ writeHead: sinon.stub(),
671
+ headersSent: false,
672
+ };
673
+
674
+ // setup a few expected results. these can be modified as needed by
675
+ // different tests.
676
+ expectedReqData = {
677
+ method: req.method,
678
+ headers: req.rawHeaders.map((h, i) => i & 1 ? h : h.toLowerCase()),
679
+ rawUrl: req.url,
680
+ pathname: '/',
681
+ search: '',
682
+ };
683
+ // it's the search string in request parlance but known as query string
684
+ // here.
685
+ connectInputs = Object.assign({}, expectedReqData);
686
+ delete connectInputs.search;
687
+ connectInputs.queries = expectedReqData.search;
688
+ });
689
+
690
+ // i don't see how to unit test this; xport.Server.prototype.emit()
691
+ // i.e., the real patching function. i think verifying that function
692
+ // will require an integration test of some sort.
693
+ // it('should handle requests on http, https, and http2', ...)
694
+
695
+ // initiateRequestHandling() sets up everything but the inputs for all analysis
696
+ // of this request.
697
+ it('handleRequestConnect works as expected', function() {
698
+ const sourceContext = core.protect.makeSourceContext(req, res);
699
+ sinon.spy(sourceContext, 'block');
700
+
701
+ const connectInputs = {
702
+ method: req.method,
703
+ headers: req.rawHeaders.map((h, i) => i & 1 ? h : h.toLowerCase()),
704
+ rawUrl: req.url,
705
+ pathname: '/',
706
+ search: '',
707
+ };
708
+
709
+ const block = core.protect.inputAnalysis.handleConnect(sourceContext, connectInputs);
710
+
711
+ expect(scoreRequestConnect).callCount(1);
712
+ expect(sourceContext.block).callCount(0);
713
+ expect(block).eql(['block', 'cmd-injection']);
714
+ });
715
+
716
+ it('handleRequestConnect() does not block when no attack', function() {
717
+ const sourceContext = core.protect.makeSourceContext(req, res);
718
+ sinon.spy(sourceContext, 'block');
719
+
720
+ const connectInputs = {
721
+ method: req.method,
722
+ headers: req.rawHeaders.map((h, i) => i & 1 ? h : h.toLowerCase()),
723
+ rawUrl: req.url,
724
+ pathname: '/',
725
+ search: '',
726
+ };
727
+
728
+ srcReturnValue = {
729
+ trackRequest: false,
730
+ };
731
+
732
+ core.protect.inputAnalysis.handleConnect(sourceContext, connectInputs);
733
+
734
+ expect(scoreRequestConnect).callCount(1);
735
+ expect(sourceContext.block).callCount(0);
736
+ });
737
+
738
+ const unimplemented = [
739
+ 'handleRequestEnd',
740
+ 'handleFileUploadName'
741
+ ];
742
+ for (const handler of unimplemented) {
743
+ it(`${handler}() should throw because it's not implemented`, function() {
744
+ const sourceContext = core.protect.makeSourceContext(req, res);
745
+ const { inputAnalysis } = core.protect;
746
+
747
+ expect(() => inputAnalysis[handler](sourceContext, { x: 'y' })).throws('nyi');
748
+ });
749
+ }
750
+ });
751
+
752
+ describe('handlers with real agent-lib', function() {
753
+ let core;
754
+ let al;
755
+ let req, res;
756
+ let reqEmitter;
757
+ let expectedReqData;
758
+ let connectInputs;
759
+
760
+ beforeEach(function() {
761
+ //console.log(this.currentTest.title);
762
+ core = mocks.core();
763
+ core.protect = mocks.protect();
764
+ al = core.protect.agentLib;
765
+ core.depHooks = mocks.depHooks();
766
+ core.config = mocks.config();
767
+ core.patcher = mocks.patcher();
768
+ Object.assign(core.config.protect.rules, {
769
+ 'cmd-injection': { mode: 'block' }
770
+ });
771
+ core.logger = mocks.logger();
772
+ core.scopes = mocks.scopes();
773
+
774
+ // return the real agentLib instantiation.
775
+ sinon.stub(Protect, 'instantiateAgentLib').returns(al);
776
+
777
+ core.protect = Protect(core);
778
+
779
+ sinon.spy(core.protect.inputAnalysis, 'handleConnect');
780
+
781
+ // mock req, res
782
+ reqEmitter = new EventEmitter();
783
+ req = {
784
+ url: '/',
785
+ method: 'GET',
786
+ rawHeaders: ['host', 'vogon.com', 'content-type', 'text/json'],
787
+ // this needs to look sort of like a real incoming message
788
+ emit: (...args) => reqEmitter.emit(...args),
789
+ _events: {},
790
+ socket: { _readableState: { autoDestroy: false } },
791
+ resume: sinon.stub(),
792
+ };
793
+ res = {
794
+ end: sinon.stub(),
795
+ writeHead: sinon.stub(),
796
+ headersSent: false,
797
+ };
798
+
799
+ // setup a few expected results. these can be modified as needed by
800
+ // different tests.
801
+ expectedReqData = {
802
+ method: req.method,
803
+ headers: req.rawHeaders.map((h, i) => i & 1 ? h : h.toLowerCase()),
804
+ rawUrl: req.url,
805
+ pathname: '/',
806
+ search: '',
807
+ };
808
+ // it's the search string in request parlance but known as query string
809
+ // here.
810
+ connectInputs = Object.assign({}, expectedReqData);
811
+ delete connectInputs.search;
812
+ connectInputs.queries = expectedReqData.search;
813
+ });
814
+
815
+ describe('handleParsedBody()', function() {
816
+ it('logs at debug level if the body is not an object', function() {
817
+ const sourceContext = core.protect.makeSourceContext(req, res);
818
+ sourceContext.rules.agentLibRules['reflected-xss'] = { mode: 'block' };
819
+ sourceContext.rules.agentLibRulesMask = al.RuleType['reflected-xss'];
820
+ sinon.spy(sourceContext, 'block');
821
+ const { inputAnalysis } = core.protect;
822
+
823
+ const parsedBody = 'not-an-object';
824
+ inputAnalysis.handleParsedBody(sourceContext, parsedBody);
825
+
826
+ const text = 'handleParsedBody() called with non-object';
827
+ expect(core.logger.debug).callCount(1).calledWith({ parsedBody }, text);
828
+ });
829
+
830
+ it('scores the object as JSON', function() {
831
+ const sourceContext = core.protect.makeSourceContext(req, res);
832
+ sourceContext.rules.agentLibRules['nosql-injection-mongo'] = { mode: 'block' };
833
+ sourceContext.rules.agentLibRulesMask |= al.RuleType['nosql-injection-mongo'];
834
+ const body = {
835
+ xyzzy: { $ne: { x: 'y' } }
836
+ };
837
+ const { inputAnalysis } = core.protect;
838
+
839
+ inputAnalysis.handleParsedBody(sourceContext, body);
840
+ checkNosqlHandleBodyResults(sourceContext, al, body, { mongoContext: true });
841
+ });
842
+
843
+ it('scores the object as urlencoded', function() {
844
+ const sourceContext = core.protect.makeSourceContext(req, res);
845
+ sourceContext.rules.agentLibRules['nosql-injection-mongo'] = { mode: 'block' };
846
+ sourceContext.rules.agentLibRulesMask |= al.RuleType['nosql-injection-mongo'];
847
+ sourceContext.reqData.contentType = 'x-www-form-urlencoded';
848
+ const body = {
849
+ xyzzy: { $ne: { x: 'y' } }
850
+ };
851
+ const { inputAnalysis } = core.protect;
852
+
853
+ inputAnalysis.handleParsedBody(sourceContext, body);
854
+ const options = { mongoContext: true, type: 'urlencoded' };
855
+ checkNosqlHandleBodyResults(sourceContext, al, body, options);
856
+ });
857
+ });
858
+ });
859
+ });
860
+
861
+ function checkNosqlHandleBodyResults(sourceContext, agentLib, body, opts = {}) {
862
+ let keyType = 'JsonKey';
863
+ let bodyType = 'json';
864
+ if (opts.type === 'urlencoded') {
865
+ keyType = 'ParameterKey';
866
+ bodyType = 'urlencoded';
867
+ }
868
+ let obj = body;
869
+ const path = [];
870
+ let next = Object.keys(body)[0];
871
+ while (next !== '$ne' && next !== '$finalize') {
872
+ path.push(next);
873
+ obj = obj[next];
874
+ next = Object.keys(obj)[0];
875
+ }
876
+ const key = next;
877
+ const inputToCheck = obj[next];
878
+
879
+ expect(sourceContext.findings.trackRequest).equal(true);
880
+ expect(sourceContext.findings.securityException).equal(undefined);
881
+ expect(sourceContext.findings.bodyType).equal(bodyType);
882
+ expect(sourceContext.findings.resultsMap).an('object').keys('nosql-injection-mongo');
883
+
884
+ const nosql = sourceContext.findings.resultsMap['nosql-injection-mongo'];
885
+ expect(nosql).an('array').length(1);
886
+ expect(nosql[0].ruleId).equal('nosql-injection-mongo');
887
+ expect(nosql[0].inputType).equal(keyType);
888
+ expect(nosql[0].path).an('array').eql(path);
889
+ expect(nosql[0].value).equal(agentLib.getMongoQueryType(key));
890
+ expect(nosql[0].score).equal(10);
891
+ expect(nosql[0].mappedId).equal('nosql-injection');
892
+ expect(nosql[0].blocked).equal(false);
893
+ expect(nosql[0].details).eql([]);
894
+
895
+ if (opts.mongoContext) {
896
+ expect(nosql[0].mongoContext).an('object').keys('inputToCheck').eql({ inputToCheck });
897
+ }
898
+ }