@contrast/protect 1.53.1 → 1.54.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 (58) hide show
  1. package/package.json +15 -12
  2. package/lib/error-handlers/common-handler.test.js +0 -52
  3. package/lib/error-handlers/index.test.js +0 -32
  4. package/lib/error-handlers/init-domain.test.js +0 -22
  5. package/lib/error-handlers/install/express.test.js +0 -290
  6. package/lib/error-handlers/install/fastify.test.js +0 -130
  7. package/lib/error-handlers/install/hapi.test.js +0 -102
  8. package/lib/error-handlers/install/koa2.test.js +0 -83
  9. package/lib/error-handlers/install/restify.test.js +0 -57
  10. package/lib/get-source-context.test.js +0 -35
  11. package/lib/hardening/handlers.test.js +0 -89
  12. package/lib/hardening/index.test.js +0 -31
  13. package/lib/hardening/install/node-serialize0.test.js +0 -58
  14. package/lib/index.test.js +0 -53
  15. package/lib/input-analysis/handlers.test.js +0 -1604
  16. package/lib/input-analysis/index.test.js +0 -45
  17. package/lib/input-analysis/install/body-parser1.test.js +0 -134
  18. package/lib/input-analysis/install/busboy1.test.js +0 -81
  19. package/lib/input-analysis/install/cookie-parser1.test.js +0 -144
  20. package/lib/input-analysis/install/express.test.js +0 -241
  21. package/lib/input-analysis/install/fastify.test.js +0 -96
  22. package/lib/input-analysis/install/formidable1.test.js +0 -114
  23. package/lib/input-analysis/install/hapi.test.js +0 -292
  24. package/lib/input-analysis/install/http.test.js +0 -270
  25. package/lib/input-analysis/install/koa-body5.test.js +0 -92
  26. package/lib/input-analysis/install/koa-bodyparser4.test.js +0 -92
  27. package/lib/input-analysis/install/koa2.test.js +0 -259
  28. package/lib/input-analysis/install/multer1.test.js +0 -209
  29. package/lib/input-analysis/install/qs6.test.js +0 -79
  30. package/lib/input-analysis/install/restify.test.js +0 -98
  31. package/lib/input-analysis/install/universal-cookie4.test.js +0 -70
  32. package/lib/input-analysis/ip-analysis.test.js +0 -71
  33. package/lib/input-analysis/virtual-patches.test.js +0 -106
  34. package/lib/input-tracing/handlers/index.test.js +0 -1236
  35. package/lib/input-tracing/index.test.js +0 -62
  36. package/lib/input-tracing/install/child-process.test.js +0 -133
  37. package/lib/input-tracing/install/eval.test.js +0 -78
  38. package/lib/input-tracing/install/fs.test.js +0 -108
  39. package/lib/input-tracing/install/function.test.js +0 -81
  40. package/lib/input-tracing/install/http.test.js +0 -85
  41. package/lib/input-tracing/install/http2.test.js +0 -83
  42. package/lib/input-tracing/install/marsdb.test.js +0 -126
  43. package/lib/input-tracing/install/mongodb.test.js +0 -280
  44. package/lib/input-tracing/install/mssql.test.js +0 -81
  45. package/lib/input-tracing/install/mysql.test.js +0 -108
  46. package/lib/input-tracing/install/postgres.test.js +0 -117
  47. package/lib/input-tracing/install/sequelize.test.js +0 -78
  48. package/lib/input-tracing/install/spdy.test.js +0 -76
  49. package/lib/input-tracing/install/sqlite3.test.js +0 -88
  50. package/lib/input-tracing/install/vm.test.js +0 -176
  51. package/lib/make-response-blocker.test.js +0 -99
  52. package/lib/make-source-context.test.js +0 -219
  53. package/lib/policy.test.js +0 -446
  54. package/lib/semantic-analysis/handlers.test.js +0 -379
  55. package/lib/semantic-analysis/index.test.js +0 -38
  56. package/lib/semantic-analysis/install/libxmljs.test.js +0 -156
  57. package/lib/semantic-analysis/utils/xml-analysis.test.js +0 -156
  58. package/lib/throw-security-exception.test.js +0 -37
@@ -1,1604 +0,0 @@
1
- 'use strict';
2
-
3
- const sinon = require('sinon');
4
- const { expect } = require('chai');
5
- const mocks = require('@contrast/test/mocks');
6
- const { createEmptySourceContext } = require('@contrast/test/fixtures');
7
- const emptySc = createEmptySourceContext();
8
- const address = require('ipaddr.js');
9
- const agentLib = require('@contrast/agent-lib');
10
- const EventEmitter = require('events');
11
- const Protect = require('../../../protect');
12
-
13
- const { constants: { RuleType, InputType } } = agentLib;
14
- const { UrlParameter, MultipartName, ParameterKey, ParameterValue } = InputType;
15
- const preferWW = { preferWorthWatching: true };
16
- const sqlMask = RuleType['sql-injection'];
17
- const xssMask = RuleType['reflected-xss'];
18
- const mongoMask = RuleType['nosql-injection-mongo'];
19
- const unsafeFileUploadMask = RuleType['unsafe-file-upload'];
20
- const { InputType: { METHOD }, Rule, ProtectRuleMode, BLOCKING_MODES } = require('@contrast/common');
21
- const { initProtectFixture } = require('@contrast/test/fixtures');
22
-
23
- /* eslint-disable newline-per-chained-call */
24
-
25
- describe('protect input-analysis handlers', function () {
26
- let core,
27
- sourceContext,
28
- handleConnect,
29
- handleQueryParams,
30
- handleUrlParams,
31
- handleCookies,
32
- handleParsedBody,
33
- handleVirtualPatches,
34
- handleFileUploadName,
35
- handleIpAllowlist,
36
- handleIpDenylist,
37
- throwSecurityException;
38
-
39
- beforeEach(function () {
40
- core = {
41
- config: mocks.config(),
42
- protect: mocks.protect(),
43
- logger: mocks.logger()
44
- };
45
- require('./handlers')(core);
46
- ({
47
- protect: {
48
- inputAnalysis: {
49
- handleConnect,
50
- handleQueryParams,
51
- handleUrlParams,
52
- handleCookies,
53
- handleParsedBody,
54
- handleFileUploadName,
55
- handleVirtualPatches,
56
- handleIpAllowlist,
57
- handleIpDenylist,
58
- },
59
- throwSecurityException
60
- }
61
- } = core);
62
-
63
- sourceContext = {
64
- ...emptySc,
65
- policy: {
66
- ...emptySc.policy,
67
- rulesMask: sqlMask,
68
- 'sql-injection': 'block',
69
- },
70
- reqData: {
71
- ip: '127.0.0.1',
72
- method: 'GET',
73
- uriPath: '/',
74
- search: '',
75
- headers: { 'content-type': 'application/json' },
76
- contentType: '',
77
- },
78
- virtualPatchesEvaluators: [],
79
- trackRequest: false,
80
- securityException: undefined,
81
- bodyType: undefined,
82
- resultsMap: Object.create(null),
83
- };
84
- });
85
-
86
- describe('handleConnect', function () {
87
- let connectInputs;
88
-
89
- beforeEach(function () {
90
- connectInputs = {
91
- headers: ['content-type', '.25 beretta', 'user-agent', '; drop table *'],
92
- };
93
- sinon.spy(core.protect.agentLib, 'scoreRequestConnect');
94
- sinon.spy(core.protect.inputAnalysis, 'handleVirtualPatches');
95
- });
96
-
97
- it('does not block a request with an attack for a worth-watching rule', function () {
98
- handleConnect(sourceContext, connectInputs);
99
-
100
- const expectedReturnValue = {
101
- trackRequest: true,
102
- resultsList: [{
103
- ruleId: 'sql-injection',
104
- inputType: 'HeaderValue',
105
- path: ['user-agent'],
106
- key: 'user-agent',
107
- value: '; drop table *',
108
- score: 10,
109
- // This is only for `agent-lib` v5.0.0,
110
- // this id will be removed in the next release
111
- idsList: [],
112
- // added by "normalizeFindings"
113
- exploitMetadata: [],
114
- blocked: false,
115
- mappedId: 'sql-injection',
116
- }],
117
- };
118
- const { agentLib } = core.protect;
119
- expect(agentLib.scoreRequestConnect).to.have.been.calledOnceWith(sqlMask, connectInputs, preferWW);
120
- // i don't know why i can't use expect(core.protect...scoreRequestConnect).returned(expected...);
121
- const rv = agentLib.scoreRequestConnect.getCall(0).returnValue;
122
- expect(rv).to.deep.equal(expectedReturnValue);
123
-
124
- // it calls with worth watching, so only reflected-xss can return a 90
125
- expect(throwSecurityException).not.to.have.been.called;
126
- });
127
-
128
- // non-worth-watching rules
129
- ['path-traversal', 'reflected-xss', 'method-tampering'].forEach((ruleId) => {
130
- it(`blocks an attack for non-worth-watching rule: ${ruleId}`, function () {
131
- const { agentLib } = core.protect;
132
- sourceContext.policy = {
133
- ...emptySc.policy,
134
- rulesMask: agentLib.RuleType[ruleId],
135
- [ruleId]: 'block'
136
- };
137
- const target = {
138
- 'path-traversal': 'queries',
139
- 'reflected-xss': 'queries',
140
- 'method-tampering': 'method',
141
- };
142
- const attack = {
143
- 'path-traversal': ['x', '../..'],
144
- 'reflected-xss': ['x', '<script>'],
145
- 'method-tampering': 'XYZZY',
146
- };
147
- connectInputs[target[ruleId]] = attack[ruleId];
148
-
149
- const block = handleConnect(sourceContext, connectInputs);
150
-
151
- expect(agentLib.scoreRequestConnect).to.have.been.calledOnceWith(agentLib.RuleType[ruleId], connectInputs, preferWW);
152
- expect(throwSecurityException).not.to.have.been.called;
153
- expect(block).to.deep.equal(['block', ruleId]);
154
- });
155
- });
156
-
157
- it('allows a request if it is matched by an exception', function () {
158
- sourceContext.policy = {
159
- rulesMask: core.protect.agentLib.RuleType['path-traversal'],
160
- 'path-traversal': 'block',
161
- exclusions: {
162
- ...emptySc.policy.exclusions,
163
- cookie: [{
164
- name: 'testPolicy',
165
- matchesUriPath: () => true,
166
- matchesInputName: (inputName) => inputName === 'first',
167
- checkCookiesInHeader: (header) => header.includes('first'),
168
- policy: { 'path-traversal': 'off' }
169
- }]
170
- }
171
- };
172
- const headers = [
173
- 'cookie', 'first=../..;', 'second', '; drop table *'
174
- ];
175
-
176
- const block = handleConnect(sourceContext, { headers });
177
-
178
- expect(block).to.have.be.undefined;
179
- });
180
- });
181
-
182
- describe('handleQueryParams', function () {
183
- beforeEach(function () {
184
- sinon.spy(core.protect.agentLib, 'scoreAtom');
185
- sinon.spy(core.protect.agentLib, 'isMongoQueryType');
186
- });
187
-
188
- it('logs at debug level if the queryParams is not an object', function () {
189
- const queryParams = 'Not an object';
190
- const text = 'handleQueryParams() called with non-object';
191
- const { agentLib } = core.protect;
192
-
193
- const result = handleQueryParams(sourceContext, queryParams);
194
-
195
- expect(agentLib.scoreAtom).not.to.have.been.called;
196
- expect(throwSecurityException).not.to.have.been.called;
197
- expect(core.logger.debug).to.have.been.calledOnceWith({ queryParams }, text);
198
- expect(result).to.be.undefined;
199
- });
200
-
201
- it('just returns if the queryParams have been already handled', function () {
202
- const queryParams = { test: 'params' };
203
- const { agentLib } = core.protect;
204
-
205
- handleQueryParams(sourceContext, queryParams);
206
- const result = handleQueryParams(sourceContext, queryParams);
207
-
208
- expect(agentLib.scoreAtom).not.to.have.been.calledOnce;
209
- expect(throwSecurityException).not.to.have.been.called;
210
- expect(core.logger.debug).not.to.have.been.called;
211
- expect(result).to.be.undefined;
212
- });
213
-
214
- it('does not block a request with an attack for a worth-watching rule', function () {
215
- const queryParams = {
216
- first: { a: { nested: '; drop table *' } },
217
- second: '',
218
- third: 'times-a-charm',
219
- };
220
- const expectedCalls = [
221
- { type: ParameterKey, value: 'first', rv: undefined },
222
- { type: ParameterKey, value: 'a', rv: undefined },
223
- { type: ParameterKey, value: 'nested', rv: undefined },
224
- { type: ParameterValue, value: '; drop table *', rv: [{ ruleId: 'sql-injection', score: 10, idsList: [] }] },
225
- { type: ParameterKey, value: 'second', rv: undefined },
226
- { type: ParameterKey, value: 'third', rv: undefined },
227
- { type: ParameterValue, value: 'times-a-charm', rv: [{ ruleId: 'sql-injection', score: 10, idsList: [] }] },
228
-
229
- ];
230
- handleQueryParams(sourceContext, queryParams);
231
-
232
- const { agentLib } = core.protect;
233
- // note that the empty string for the key 'second' does not generate a callback in handleQueryParams.
234
- expect(agentLib.scoreAtom).to.have.callCount(7);
235
-
236
- for (let i = 0; i < expectedCalls.length; i++) {
237
- const exp = expectedCalls[i];
238
- const call = agentLib.scoreAtom.getCall(i);
239
- expect(call).to.have.been.calledWith(sqlMask, exp.value, exp.type, preferWW);
240
- expect(call.returnValue).to.deep.equal(exp.rv);
241
- }
242
-
243
- // it calls with worth watching, so only reflected-xss can return a 90
244
- expect(throwSecurityException).not.to.have.been.called;
245
- });
246
-
247
- it('blocks a request with a definite attack for a non-worth-watching rule', function () {
248
- const xssMask = RuleType['reflected-xss'];
249
- sourceContext.policy = {
250
- ...emptySc.policy,
251
- rulesMask: xssMask,
252
- 'reflected-xss': 'block',
253
- };
254
- const queryParams = {
255
- first: { a: { nested: '<script>' } },
256
- second: '',
257
- third: 'eval(x.toString())',
258
- };
259
-
260
- const expectedCalls = [
261
- { type: ParameterKey, value: 'first', rv: undefined },
262
- { type: ParameterKey, value: 'a', rv: undefined },
263
- { type: ParameterKey, value: 'nested', rv: undefined },
264
- { type: ParameterValue, value: '<script>', rv: [{ ruleId: 'reflected-xss', score: 90, idsList: ['PIDS-XSS-38A'] }] },
265
- { type: ParameterKey, value: 'second', rv: undefined },
266
- { type: ParameterKey, value: 'third', rv: undefined },
267
- { type: ParameterValue, value: 'eval(x.toString())', rv: [{ ruleId: 'reflected-xss', score: 90, idsList: ['BAD-JS-FUNCTION-CALL-1'] }] },
268
- ];
269
- handleQueryParams(sourceContext, queryParams);
270
-
271
- const { agentLib } = core.protect;
272
- // note that the empty string for the key 'second' does not generate a callback in handleQueryParams.
273
- expect(agentLib.scoreAtom).to.have.callCount(7);
274
-
275
- for (let i = 0; i < expectedCalls.length; i++) {
276
- const exp = expectedCalls[i];
277
- const call = agentLib.scoreAtom.getCall(i);
278
- expect(call).to.have.been.calledWith(xssMask, exp.value, exp.type, preferWW);
279
- expect(call.returnValue).to.deep.equal(exp.rv);
280
- }
281
-
282
- // scoreAtom is called with worth watching, so only reflected-xss can return a 90
283
- expect(throwSecurityException).to.have.been.calledOnceWith(sourceContext);
284
- });
285
-
286
- it('allows a request if it is matched by an exception', function () {
287
- const xssMask = RuleType['reflected-xss'];
288
- sourceContext.policy = {
289
- rulesMask: xssMask,
290
- 'reflected-xss': 'block',
291
- exclusions: {
292
- ...emptySc.policy.exclusions,
293
- parameter: [{
294
- name: 'testPolicy',
295
- matchesUriPath: () => true,
296
- matchesInputName: (inputName) => inputName === 'nested',
297
- policy: { 'reflected-xss': 'off' }
298
- }]
299
- }
300
- };
301
- const queryParams = {
302
- first: { a: { nested: '<script>' } },
303
- second: '',
304
- };
305
-
306
- handleQueryParams(sourceContext, queryParams);
307
-
308
- expect(throwSecurityException).not.to.have.been.called;
309
- });
310
-
311
- it('handles nosql-injection-mongo', function () {
312
- const queryParams = {
313
- first: { user: { $eq: { id: '*' } } }
314
- };
315
- sourceContext.policy = {
316
- ...emptySc.policy,
317
- rulesMask: mongoMask,
318
- 'nosql-injection-mongo': 'block',
319
- };
320
- const { agentLib } = core.protect;
321
-
322
- handleQueryParams(sourceContext, queryParams);
323
-
324
- expect(agentLib.scoreAtom).to.have.callCount(5);
325
- expect(agentLib.isMongoQueryType).to.have.callCount(4);
326
-
327
- expect(sourceContext.trackRequest).to.be.true;
328
- expect(sourceContext.securityException).to.be.undefined;
329
- expect(sourceContext.resultsMap).an('object').keys('nosql-injection-mongo');
330
-
331
- const results = sourceContext.resultsMap['nosql-injection-mongo'];
332
- expect(results).an('array').length(1);
333
-
334
- const result = results[0];
335
- expect(result.path).to.deep.equal(['first', 'user']);
336
- expect(result.inputType).to.equal('ParameterKey');
337
- expect(result.key).to.equal('$eq');
338
- expect(result.value).to.equal('$eq');
339
- expect(result.mongoContext).an('object').keys('inputToCheck');
340
-
341
- const context = result.mongoContext;
342
- expect(context.inputToCheck).to.deep.equal({ id: '*' });
343
- });
344
-
345
- it('handles nested nosql-injection-mongo results', function () {
346
- const queryParams = {
347
- first: { user: { $eq: { id: { $ne: { password: '' } } } } }
348
- };
349
- sourceContext.policy = {
350
- ...emptySc.policy,
351
- rulesMask: mongoMask,
352
- 'nosql-injection-mongo': 'block',
353
- };
354
- const { agentLib } = core.protect;
355
-
356
- handleQueryParams(sourceContext, queryParams);
357
-
358
- expect(agentLib.scoreAtom).to.have.callCount(6);
359
- expect(agentLib.isMongoQueryType).to.have.callCount(6);
360
-
361
- expect(sourceContext.trackRequest).to.be.true;
362
- expect(sourceContext.securityException).to.be.undefined;
363
- expect(sourceContext.resultsMap).an('object').keys('nosql-injection-mongo');
364
-
365
- const results = sourceContext.resultsMap['nosql-injection-mongo'];
366
- expect(results).an('array').length(2);
367
-
368
- const exp = [{
369
- path: ['first', 'user'],
370
- key: '$eq',
371
- inputToCheck: { id: { $ne: { password: '' } } },
372
- }, {
373
- path: ['first', 'user', '$eq', 'id'],
374
- key: '$ne',
375
- inputToCheck: { password: '' },
376
- }];
377
-
378
- for (let i = 0; i < exp.length; i++) {
379
- expect(results[i].path).to.deep.equal(exp[i].path);
380
- expect(results[i].inputType).to.equal('ParameterKey');
381
- expect(results[i].key).to.equal(exp[i].key);
382
- expect(results[i].value).to.equal(exp[i].key);
383
- expect(results[i].mongoContext).an('object').keys('inputToCheck');
384
-
385
- expect(results[i].mongoContext.inputToCheck).to.deep.equal(exp[i].inputToCheck);
386
- }
387
- });
388
-
389
- it('handles multiple nosql-injection-mongo results', function () {
390
- const queryParams = {
391
- first: {
392
- user: { $where: { id: '*' } },
393
- password: { $finalize: 'while(1){};' },
394
- }
395
- };
396
- sourceContext.policy = {
397
- ...emptySc.policy,
398
- rulesMask: mongoMask,
399
- 'nosql-injection-mongo': 'block',
400
- };
401
- const { agentLib } = core.protect;
402
-
403
- handleQueryParams(sourceContext, queryParams);
404
-
405
- expect(agentLib.scoreAtom).to.have.callCount(8);
406
- expect(agentLib.isMongoQueryType).to.have.callCount(6);
407
-
408
- expect(sourceContext.trackRequest).to.equal(true, 'trackRequest should be true');
409
- expect(sourceContext.securityException).to.be.undefined;
410
- expect(sourceContext.resultsMap).an('object').keys('nosql-injection-mongo');
411
-
412
- const results = sourceContext.resultsMap['nosql-injection-mongo'];
413
- expect(results).an('array').length(2);
414
-
415
- const exp = [{
416
- path: ['first', 'user'],
417
- key: '$where',
418
- inputToCheck: { id: '*' },
419
- }, {
420
- path: ['first', 'password'],
421
- key: '$finalize',
422
- inputToCheck: 'while(1){};',
423
- }];
424
-
425
- for (let i = 0; i < exp.length; i++) {
426
- expect(results[i].path).to.deep.equal(exp[i].path);
427
- expect(results[i].inputType).to.equal('ParameterKey');
428
- expect(results[i].key).to.equal(exp[i].key);
429
- expect(results[i].value).to.equal(exp[i].key);
430
- expect(results[i].mongoContext).an('object').keys('inputToCheck');
431
-
432
- expect(results[i].mongoContext.inputToCheck).to.deep.equal(exp[i].inputToCheck);
433
- }
434
- });
435
- });
436
-
437
- describe('handleUrlParams', function () {
438
- beforeEach(function () {
439
- sinon.spy(core.protect.agentLib, 'scoreAtom');
440
- });
441
-
442
- it('logs at debug level if the urlParams is not an object', function () {
443
- const urlParams = 'Not an object';
444
- const text = 'handleUrlParams() called with non-object';
445
- const { agentLib } = core.protect;
446
-
447
- const result = handleUrlParams(sourceContext, urlParams);
448
-
449
- expect(agentLib.scoreAtom).not.to.have.been.called;
450
- expect(throwSecurityException).not.to.have.been.called;
451
- expect(core.logger.debug).to.have.been.calledOnceWith({ urlParams }, text);
452
- expect(result).to.be.undefined;
453
- });
454
-
455
- it('just returns if the urlParams have been already handled', function () {
456
- const urlParams = { test: 'params' };
457
- const { agentLib } = core.protect;
458
-
459
- handleUrlParams(sourceContext, urlParams);
460
- const result = handleUrlParams(sourceContext, urlParams);
461
-
462
- expect(agentLib.scoreAtom).to.have.been.calledOnce;
463
- expect(throwSecurityException).not.to.have.been.called;
464
- expect(core.logger.debug).not.to.have.been.called;
465
- expect(result).to.be.undefined;
466
- });
467
-
468
- it('does not block a request when no valid results are found', function () {
469
- const urlParams = {
470
- first: 'a',
471
- second: '',
472
- third: 'b',
473
- };
474
- const result = handleUrlParams(sourceContext, urlParams);
475
-
476
- const { agentLib } = core.protect;
477
- expect(agentLib.scoreAtom).to.have.been.calledTwice;
478
- expect(agentLib.scoreAtom.firstCall).to.have.been.calledWith(sqlMask, 'a', UrlParameter, preferWW);
479
- expect(agentLib.scoreAtom.secondCall).to.have.been.calledWith(sqlMask, 'b', UrlParameter, preferWW);
480
-
481
- expect(throwSecurityException).not.to.have.been.called;
482
- expect(result).to.be.undefined;
483
- });
484
-
485
- it('does not block a request with an attack for a worth-watching rule', function () {
486
- const urlParams = {
487
- first: { a: { nested: '; drop table *' } },
488
- second: '',
489
- third: 'times-a-charm',
490
- };
491
- handleUrlParams(sourceContext, urlParams);
492
-
493
- const { agentLib } = core.protect;
494
- // scoreAtom is only called from leaf, not key, values that are not falsey.
495
- expect(agentLib.scoreAtom).to.have.been.calledTwice;
496
- expect(agentLib.scoreAtom.firstCall).to.have.been.calledWith(sqlMask, '; drop table *', UrlParameter, preferWW);
497
- expect(agentLib.scoreAtom.secondCall).to.have.been.calledWith(sqlMask, 'times-a-charm', UrlParameter, preferWW);
498
-
499
- for (let i = 0; i < 2; i++) {
500
- const rv = agentLib.scoreAtom.getCall(i).returnValue;
501
- expect(rv).to.deep.equal([{ ruleId: 'sql-injection', score: 10, idsList: [] }]);
502
- }
503
-
504
- // it calls with worth watching, so only reflected-xss can return a 90
505
- expect(throwSecurityException).not.to.have.been.called;
506
- });
507
-
508
- it('blocks a request with a definite attack for a non-worth-watching rule', function () {
509
- const xssMask = RuleType['reflected-xss'];
510
- sourceContext.policy = {
511
- rulesMask: xssMask,
512
- 'reflected-xss': 'block',
513
- };
514
- const urlParams = {
515
- first: { a: { nested: '<script>' } },
516
- second: '',
517
- third: 'eval(x.toString())',
518
- };
519
-
520
- handleUrlParams(sourceContext, urlParams);
521
-
522
- const { agentLib } = core.protect;
523
- // scoreAtom is only called from leaf, not key, values that are not falsey.
524
- expect(agentLib.scoreAtom).to.have.been.calledTwice;
525
- expect(agentLib.scoreAtom.firstCall).to.have.been.calledWith(xssMask, '<script>', UrlParameter, preferWW);
526
- expect(agentLib.scoreAtom.secondCall).to.have.been.calledWith(xssMask, 'eval(x.toString())', UrlParameter, preferWW);
527
-
528
- const expectedCalls = [
529
- { type: ParameterKey, value: '<script>', rv: [{ ruleId: 'reflected-xss', score: 90, idsList: ['PIDS-XSS-38A'] }] },
530
- { type: ParameterValue, value: 'eval(x.toString())', rv: [{ ruleId: 'reflected-xss', score: 90, idsList: ['BAD-JS-FUNCTION-CALL-1'] }] },
531
- ];
532
-
533
- for (let i = 0; i < expectedCalls.length; i++) {
534
- const exp = expectedCalls[i];
535
- const rv = agentLib.scoreAtom.getCall(i).returnValue;
536
- expect(rv).to.deep.equal(exp.rv);
537
- }
538
-
539
- // it calls with worth watching, so only reflected-xss can return a 90
540
- expect(throwSecurityException).to.have.been.calledOnceWith(sourceContext);
541
- });
542
-
543
- // there are no longer any worth-watching returns from a non-worth-watching rule. there
544
- // used to be, but there are now. this test is skipped (as opposed to removed) to serve
545
- // as a guide in case this can happen in the future.
546
- it.skip('does not block a request for a worth-watching score from a non-worth-watching rule', function () {
547
- sourceContext.policy = {
548
- rulesMask: xssMask,
549
- 'reflected-xss': 'block',
550
- };
551
- // this is the only Score::HIGH pattern left; if it goes away then this
552
- // test should go away too.
553
- const tenScore1 = 'type="text/javascript"';
554
- const tenScore2 = 'type="application/javascript"';
555
- const urlParams = {
556
- first: { a: { nested: tenScore1 } },
557
- second: '',
558
- third: tenScore2,
559
- };
560
-
561
- handleUrlParams(sourceContext, urlParams);
562
-
563
- const { agentLib } = core.protect;
564
- // scoreAtom is only called from leaf, not key, values that are not falsey.
565
- expect(agentLib.scoreAtom).to.have.been.calledTwice;
566
- expect(agentLib.scoreAtom.firstCall).to.have.been.calledWith(xssMask, tenScore1, UrlParameter, preferWW);
567
- expect(agentLib.scoreAtom.secondCall).to.have.been.calledWith(xssMask, tenScore2, UrlParameter, preferWW);
568
-
569
- for (let i = 0; i < 2; i++) {
570
- const rv = agentLib.scoreAtom.getCall(i).returnValue;
571
- expect(rv).to.deep.equal([{ ruleId: 'reflected-xss', score: 10 }]);
572
- }
573
-
574
- // while reflected-xss can return a 90 score, these particular attacks only
575
- // score 10, so block shouldn't be called.
576
- expect(throwSecurityException).not.to.have.been.called;
577
- expect(sourceContext.resultsMap).an('object').keys(['reflected-xss']);
578
- const expected = {
579
- path: [['first', 'a', 'nested'], ['third']],
580
- key: ['nested', 'third'],
581
- value: [tenScore1, tenScore2]
582
- };
583
- expect(sourceContext.resultsMap).an('object').keys(['reflected-xss']);
584
- const reflectedXssResults = sourceContext.resultsMap['reflected-xss'];
585
-
586
- for (let i = 0; i < reflectedXssResults.length; i++) {
587
- const result = reflectedXssResults[i];
588
- expect(result.ruleId).to.equal('reflected-xss');
589
- expect(result.inputType).to.equal('UrlParameter');
590
- expect(result.path).to.deep.equal(expected.path[i]);
591
- expect(result.key).to.equal(expected.key[i]);
592
- expect(result.value).to.equal(expected.value[i]);
593
- }
594
- });
595
- });
596
-
597
- describe('handleCookies', function () {
598
- const sql10Score = '; drop table *';
599
- let al;
600
-
601
- beforeEach(function () {
602
- al = core.protect.agentLib;
603
- });
604
-
605
- it('does not block the request because worth-watching is used', function () {
606
- const cookies = {
607
- secret: sql10Score,
608
- };
609
-
610
- const expectedInputs = { cookies: [] };
611
- Object.entries(cookies).forEach(entry => expectedInputs.cookies.push(...entry));
612
- sinon.spy(al, 'scoreRequestConnect');
613
-
614
- handleCookies(sourceContext, cookies);
615
-
616
- expect(al.scoreRequestConnect).to.have.been.calledOnceWith(sqlMask, expectedInputs, preferWW);
617
- expect(throwSecurityException).not.to.have.been.called;
618
- });
619
-
620
- it('just returns if the cookies have been already handled', function () {
621
- const cookies = {
622
- test: 'cookie',
623
- };
624
- sinon.spy(al, 'scoreRequestConnect');
625
-
626
- handleCookies(sourceContext, cookies);
627
- const result = handleCookies(sourceContext, cookies);
628
-
629
-
630
- expect(al.scoreRequestConnect).to.have.been.calledOnce;
631
- expect(throwSecurityException).not.to.have.been.called;
632
- expect(result).to.be.undefined;
633
- });
634
-
635
- it('blocks the request when some other finding has a security exception but it is not yet thrown (failsafe)', function () {
636
- const cookies = {
637
- secret: sql10Score,
638
- };
639
- sourceContext.securityException = ['block', 'reflected-xss'];
640
- sourceContext.resultsMap = {
641
- 'reflected-xss': { ruleId: 'reflected-xss', inputType: 'ParameterKey', path: [], key: '<script>', value: '<script>', score: 90 }
642
- };
643
-
644
- const expectedInputs = { cookies: [] };
645
- Object.entries(cookies).forEach(entry => expectedInputs.cookies.push(...entry));
646
- sinon.spy(al, 'scoreRequestConnect');
647
-
648
- handleCookies(sourceContext, cookies);
649
-
650
- expect(al.scoreRequestConnect).to.have.been.calledOnceWith(sqlMask, expectedInputs, preferWW);
651
- expect(throwSecurityException).to.have.been.calledOnceWith(sourceContext);
652
- });
653
- });
654
-
655
- describe('handleParsedBody', function () {
656
- const sql10Score = '; drop table *';
657
- const xss90Score = '<script>';
658
- let al;
659
- let sc;
660
-
661
- beforeEach(function () {
662
- al = core.protect.agentLib;
663
- sc = sourceContext;
664
- sinon.spy(al, 'scoreAtom');
665
- });
666
-
667
- it('blocks the request when a 90 score for reflected-xss', function () {
668
- const rulesMask = 0 | al.RuleType['sql-injection'] | al.RuleType['reflected-xss'];
669
- sourceContext.policy = {
670
- ...emptySc.policy,
671
- rulesMask,
672
- 'reflected-xss': 'block',
673
- 'sql-injection': 'block',
674
- };
675
- const body = {
676
- secret: sql10Score,
677
- [xss90Score]: 'special',
678
- };
679
-
680
- handleParsedBody(sourceContext, body);
681
-
682
- const PK = al.InputType.ParameterKey;
683
- const PV = al.InputType.ParameterValue;
684
-
685
- expect(al.scoreAtom).to.have.callCount(4);
686
- expect(al.scoreAtom.getCall(0).args).to.deep.equal([rulesMask, 'secret', PK, preferWW]);
687
- expect(al.scoreAtom.getCall(1).args).to.deep.equal([rulesMask, sql10Score, PV, preferWW]);
688
- expect(al.scoreAtom.getCall(2).args).to.deep.equal([rulesMask, xss90Score, PK, preferWW]);
689
- expect(al.scoreAtom.getCall(3).args).to.deep.equal([rulesMask, 'special', PV, preferWW]);
690
- expect(throwSecurityException).to.have.been.calledOnceWith(sc);
691
-
692
- expect(sourceContext.resultsMap).an('object').keys(['reflected-xss', 'sql-injection']);
693
- const expected = [
694
- { ruleId: 'sql-injection', inputType: 'ParameterValue', path: ['secret'], key: 'secret', value: sql10Score, score: 10 },
695
- { ruleId: 'reflected-xss', inputType: 'ParameterKey', path: [], key: xss90Score, value: xss90Score, score: 90 },
696
- ];
697
-
698
- for (let i = 0; i < expected.length; i++) {
699
- expect(sourceContext.resultsMap[expected[i].ruleId]).an('array').length(1);
700
-
701
- const result = sourceContext.resultsMap[expected[i].ruleId][0];
702
- expect(result.ruleId).to.equal(expected[i].ruleId);
703
- expect(result.inputType).to.equal(expected[i].inputType);
704
- expect(result.path).to.deep.equal(expected[i].path);
705
- expect(result.key).to.equal(expected[i].key);
706
- expect(result.value).to.equal(expected[i].value);
707
- expect(result.score).to.equal(expected[i].score);
708
- }
709
- });
710
-
711
- it('just returns if the parsedBody have been already handled', function () {
712
- const { inputAnalysis } = core.protect;
713
-
714
- sinon.spy(inputAnalysis, 'handleVirtualPatches');
715
-
716
- const parsedBody = { test: 'body' };
717
- inputAnalysis.handleParsedBody(sourceContext, parsedBody);
718
- const result = inputAnalysis.handleParsedBody(sourceContext, parsedBody);
719
-
720
- expect(core.logger.debug).not.to.have.been.called;
721
- expect(inputAnalysis.handleVirtualPatches).to.have.been.calledOnce;
722
- expect(result).to.be.undefined;
723
- });
724
-
725
- it('does not block the request if the request is not tracked', function () {
726
- const body = { secret: sql10Score };
727
-
728
- handleParsedBody(sourceContext, body);
729
-
730
- expect(al.scoreAtom).to.have.been.calledTwice;
731
- expect(throwSecurityException).not.to.have.been.called;
732
-
733
- expect(sourceContext.resultsMap).an('object').keys(['sql-injection']);
734
- const expected = {
735
- ruleId: ['sql-injection'],
736
- inputType: ['ParameterValue'],
737
- path: [['secret']],
738
- key: ['secret'],
739
- value: [sql10Score],
740
- score: [10],
741
- };
742
- for (let i = 0; i < expected.length; i++) {
743
- expect(sourceContext.resultsMap[expected[i].ruleId]).an('array').length(1);
744
-
745
- const result = sourceContext.resultsMap[expected[i].ruleId][0];
746
- expect(result.ruleId).to.equal(expected[i].ruleId);
747
- expect(result.inputType).to.equal(expected[i].inputType);
748
- expect(result.path).to.deep.equal(expected[i].path);
749
- expect(result.key).to.equal(expected[i].key);
750
- expect(result.value).to.equal(expected[i].value);
751
- expect(result.score).to.equal(expected[i].score);
752
- }
753
- });
754
- });
755
-
756
- describe('handleFileUploadName', function () {
757
- beforeEach(function () {
758
- sourceContext.policy = { rulesMask: unsafeFileUploadMask, 'unsafe-file-upload': 'block_at_perimeter' };
759
- sinon.spy(core.protect.agentLib, 'scoreAtom');
760
- });
761
-
762
- it('logs at debug level if the filename is not a string', function () {
763
- const filenames = [1];
764
- const text = 'handleFileUploadName() was called with non-string';
765
- const { agentLib } = core.protect;
766
-
767
- const result = handleFileUploadName(sourceContext, filenames);
768
-
769
- expect(agentLib.scoreAtom).not.to.have.been.called;
770
- expect(throwSecurityException).not.to.have.been.called;
771
- expect(core.logger.debug).to.have.been.calledOnceWith({ filename: 1 }, text);
772
- expect(result).to.be.undefined;
773
- });
774
-
775
- it('does not block when the rule mode is off', function () {
776
- sourceContext.policy = { rulesMask: 0, 'unsafe-file-upload': 'off' };
777
- const filenames = ['bad-file.php'];
778
- const result = handleFileUploadName(sourceContext, filenames);
779
-
780
- const { agentLib } = core.protect;
781
- expect(agentLib.scoreAtom).not.to.have.been.called;
782
- expect(throwSecurityException).not.to.have.been.called;
783
- expect(result).to.be.undefined;
784
- });
785
-
786
- it('does not block a request when no valid results are found', function () {
787
- const filenames = ['good-file.log'];
788
- const result = handleFileUploadName(sourceContext, filenames);
789
-
790
- const { agentLib } = core.protect;
791
- expect(agentLib.scoreAtom).to.have.been.calledOnceWithExactly(unsafeFileUploadMask, 'good-file.log', MultipartName);
792
-
793
- expect(throwSecurityException).not.to.have.been.called;
794
- expect(result).to.be.undefined;
795
- });
796
-
797
- it('blocks a request with a definite attack', function () {
798
- const filenames = ['bad-file.php'];
799
-
800
- handleFileUploadName(sourceContext, filenames);
801
-
802
- const { agentLib } = core.protect;
803
- // scoreAtom is only called from leaf, not key, values that are not falsey.
804
- expect(agentLib.scoreAtom).to.have.been.calledOnceWithExactly(unsafeFileUploadMask, 'bad-file.php', MultipartName);
805
- expect(agentLib.scoreAtom.getCall(0).returnValue).to.deep.equal([{ ruleId: 'unsafe-file-upload', score: 90, idsList: ['UFU-1'] }]);
806
- expect(throwSecurityException).to.have.been.calledOnceWith(sourceContext);
807
- });
808
- });
809
-
810
- describe('handleVirtualPatches', function () {
811
- it('throws an exception when the request matches the all evaluations in the virtual patch', function () {
812
- const mockEvaluator = (value) => value === 'attack';
813
-
814
- sourceContext.virtualPatchesEvaluators = [new Map([['HEADERS', mockEvaluator], ['PARAMETERS', mockEvaluator], ['metadata', { uuid: 'uuid', name: 'name' }]])];
815
-
816
- handleVirtualPatches(sourceContext, { HEADERS: 'attack' });
817
-
818
- expect(throwSecurityException).not.to.have.been.called;
819
- handleVirtualPatches(sourceContext, { PARAMETERS: 'attack' });
820
-
821
- expect(throwSecurityException).to.have.been.called;
822
- });
823
- });
824
-
825
- describe('handleIpAllowlist', function () {
826
- let clock, ipAllowlist;
827
-
828
- beforeEach(function () {
829
- clock = sinon.useFakeTimers();
830
-
831
- ipAllowlist = [
832
- {
833
- ip: '127.0.0.1',
834
- expires: 0,
835
- doesExpire: false,
836
- expiresAt: undefined,
837
- normalizedValue: '127.0.0.1',
838
- cidr: undefined
839
- },
840
- {
841
- name: 'expired-one',
842
- ip: '1.2.3.4',
843
- expires: 50000,
844
- doesExpire: true,
845
- expiresAt: 50000,
846
- normalizedValue: '1.2.3.4',
847
- cidr: undefined
848
- }
849
- ];
850
- });
851
-
852
- afterEach(function () {
853
- clock.restore();
854
- });
855
-
856
- it('returns true if a non-expired match is found', function () {
857
- const result = handleIpAllowlist(sourceContext, ipAllowlist);
858
-
859
- expect(core.logger.info).to.have.been.calledWith({ ...ipAllowlist[0], matchedIp: sourceContext.reqData.ip }, 'Found a matching IP to an entry in ipAllow list');
860
- expect(result).to.be.true;
861
- });
862
-
863
- it('returns undefined if a the match found is expired', async function () {
864
- sourceContext.reqData.ip = '1.2.3.4';
865
- await clock.tickAsync(50000);
866
- const result = handleIpAllowlist(sourceContext, ipAllowlist);
867
-
868
- expect(core.logger.info).to.have.been.calledWith('IP expired: %s, %s', ipAllowlist[1].name, ipAllowlist[1].ip);
869
- expect(result).to.be.undefined;
870
- });
871
-
872
- it('does nothing when there is no source context, or the list is empty', function () {
873
- const results = [
874
- handleIpAllowlist(null, ipAllowlist),
875
- handleIpAllowlist(sourceContext, [])
876
- ];
877
-
878
- expect(core.logger.info).not.to.have.been.called;
879
- expect(results).to.deep.equal([undefined, undefined]);
880
- });
881
- });
882
-
883
- describe('handleIpDenylist', function () {
884
- let ipDenylist;
885
-
886
- beforeEach(function () {
887
- ipDenylist = [
888
- {
889
- ip: '192.168.1.0/12',
890
- expires: 0,
891
- doesExpire: false,
892
- expiresAt: undefined,
893
- normalizedValue: '192.168.1.0',
894
- cidr: { range: address.parseCIDR('192.168.1.0/12'), kind: 'ipv4' }
895
- }
896
- ];
897
- });
898
-
899
- it('returns true if a non-expired match is found', function () {
900
- sourceContext.reqData.headers = ['x-forwarded-for', '127.0.0.1;127.0.0.2,192.168.1.1'];
901
- const result = handleIpDenylist(sourceContext, ipDenylist);
902
-
903
- expect(core.logger.info).to.have.been.calledWith({ ...ipDenylist[0], match: '192.168.1.1' }, 'Found a matching IP to an entry in ipDeny list');
904
- expect(result).to.deep.equal(['block', 'ip-denylist']);
905
- });
906
-
907
- it('returns undefined if a no match is found', async function () {
908
- sourceContext.reqData.ip = '2001:db8:3333:4444:5555:6666:7777:8888';
909
- const result = handleIpAllowlist(sourceContext, ipDenylist);
910
-
911
- // expect(core.logger.info).to.have.been.calledWith(`IP expired: ${ipAllowlist[1].name}, ${ipAllowlist[1].ip}`);
912
- expect(result).to.be.undefined;
913
- });
914
-
915
- it('does nothing when there is no source context, or the list is empty', function () {
916
- const results = [
917
- handleIpDenylist(null, ipDenylist),
918
- handleIpDenylist(sourceContext, [])
919
- ];
920
-
921
- expect(core.logger.info).not.to.have.been.called;
922
- expect(results).to.deep.equal([undefined, undefined]);
923
- });
924
- });
925
-
926
- describe(Rule.METHOD_TAMPERING, function () {
927
- const ruleId = Rule.METHOD_TAMPERING;
928
- [ProtectRuleMode.BLOCK, ProtectRuleMode.MONITOR].forEach((mode) => {
929
- [
930
- ['acl', true],
931
- ['baseline-control', true],
932
- ['checkin', true],
933
- ['checkout', true],
934
- ['connect', true],
935
- ['copy', true],
936
- ['delete', true],
937
- ['get', true],
938
- ['head', true],
939
- ['label', true],
940
- ['lock', true],
941
- ['merge', true],
942
- ['mkactivity', true],
943
- ['mkcalendar', true],
944
- ['mkcol', true],
945
- ['mkworkspace', true],
946
- ['move', true],
947
- ['options', true],
948
- ['orderpatch', true],
949
- ['patch', true],
950
- ['post', true],
951
- ['propfind', true],
952
- ['proppatch', true],
953
- ['put', true],
954
- ['report', true],
955
- ['search', true],
956
- ['trace', true],
957
- ['uncheckout', true],
958
- ['unlock', true],
959
- ['update', true],
960
- ['version-control', true],
961
- ['attack', false],
962
- ['unknown-verb', false],
963
- ['$arbitrary', false],
964
- ].forEach(([method, safe]) => {
965
- it(`HTTP verb '${method.toUpperCase()}' is ${safe ? 'safe' : 'unsafe'}`, function () {
966
- const sourceContext = {
967
- policy: {
968
- [ruleId]: mode
969
- },
970
- resultsMap: {}
971
- };
972
- const result = core.protect.inputAnalysis.handleMethodTampering(sourceContext, { method });
973
- const blocked = BLOCKING_MODES.includes(mode);
974
-
975
- if (safe) {
976
- expect(sourceContext.resultsMap).to.be.empty;
977
- } else {
978
- expect(sourceContext.resultsMap[ruleId]).to.deep.equal([{
979
- key: 'method',
980
- value: method,
981
- blocked,
982
- exploitMetadata: null,
983
- inputType: METHOD
984
- }]);
985
- if (blocked) {
986
- expect(result).to.deep.equal(['block', ruleId]);
987
- expect(sourceContext.securityException).to.deep.equal(['block', ruleId]);
988
- }
989
- }
990
- });
991
- });
992
- });
993
- });
994
-
995
- describe('commonObjectAnalyzer', function () {
996
- it('does nothing when there are no results found in commonObjectAnalyzer()', function () {
997
- const body = {
998
- x: { y: 'z' },
999
- };
1000
- const { inputAnalysis } = core.protect;
1001
-
1002
- expect(inputAnalysis.handleParsedBody(sourceContext, body)).to.be.undefined;
1003
- });
1004
-
1005
- it.skip('returns when wrong arguments are passed to getValueAtKey()', function () {
1006
- // TODO?
1007
- });
1008
- });
1009
- });
1010
-
1011
- describe('input analysis', function () {
1012
- describe('handlers with mock agent-lib', function () {
1013
- let core,
1014
- inputAnalysis,
1015
- server,
1016
- serverEmit,
1017
- req,
1018
- res,
1019
- reqEmitter,
1020
- expectedReqData,
1021
- connectInputs,
1022
- scoreRequestConnect,
1023
- srcReturnValue;
1024
-
1025
- beforeEach(function () {
1026
- ({ core } = initProtectFixture());
1027
-
1028
- Object.assign(core.config.protect.rules, {
1029
- 'cmd-injection': { mode: 'block' }
1030
- });
1031
- core.logger = mocks.logger();
1032
- core.scopes = mocks.scopes();
1033
-
1034
- srcReturnValue = {
1035
- trackRequest: true,
1036
- resultsList: [{
1037
- score: 90.0,
1038
- ruleId: 'cmd-injection',
1039
- }],
1040
- };
1041
- scoreRequestConnect = sinon.stub().callsFake(() => srcReturnValue);
1042
-
1043
- sinon.stub(Protect, 'instantiateAgentLib').returns({
1044
- // indirection so it can be changed in each test
1045
- scoreRequestConnect,
1046
- RuleType: agentLib.constants.RuleType,
1047
- InputType: agentLib.constants.InputType,
1048
- // DbType,
1049
- });
1050
-
1051
- core.protect = Protect(core);
1052
-
1053
- sinon.spy(core.protect.inputAnalysis, 'handleConnect');
1054
-
1055
- inputAnalysis = require('./install/http')(core);
1056
- inputAnalysis.install();
1057
-
1058
- // mock Server thing...
1059
- server = {};
1060
- serverEmit = sinon.stub();
1061
- server.prototype = { emit: serverEmit };
1062
-
1063
- // mock req, res
1064
- reqEmitter = new EventEmitter();
1065
- req = {
1066
- url: '/',
1067
- method: 'GET',
1068
- rawHeaders: ['host', 'vogon.com', 'content-type', 'text/json'],
1069
- // this needs to look sort of like a real incoming message
1070
- emit: (...args) => reqEmitter.emit(...args),
1071
- _events: {},
1072
- socket: { _readableState: { autoDestroy: false } },
1073
- resume: sinon.stub(),
1074
- };
1075
- res = {
1076
- end: sinon.stub(),
1077
- writeHead: sinon.stub(),
1078
- headersSent: false,
1079
- };
1080
-
1081
- // setup a few expected results. these can be modified as needed by
1082
- // different tests.
1083
- expectedReqData = {
1084
- method: req.method,
1085
- headers: req.rawHeaders.map((h, i) => i & 1 ? h : h.toLowerCase()),
1086
- rawUrl: req.url,
1087
- pathname: '/',
1088
- search: '',
1089
- };
1090
- // it's the search string in request parlance but known as query string
1091
- // here.
1092
- connectInputs = Object.assign({}, expectedReqData);
1093
- delete connectInputs.search;
1094
- connectInputs.queries = expectedReqData.search;
1095
- });
1096
-
1097
- // i don't see how to unit test this; xport.Server.prototype.emit()
1098
- // i.e., the real patching function. i think verifying that function
1099
- // will require an integration test of some sort.
1100
- // it('should handle requests on http, https, and http2', ...)
1101
-
1102
- // initiateRequestHandling() sets up everything but the inputs for all analysis
1103
- // of this request.
1104
- it('handleRequestConnect works as expected', function () {
1105
- const sourceContext = core.protect.makeSourceContext(req, res);
1106
- sinon.spy(core.protect, 'throwSecurityException');
1107
-
1108
- const connectInputs = {
1109
- method: req.method,
1110
- headers: req.rawHeaders.map((h, i) => i & 1 ? h : h.toLowerCase()),
1111
- rawUrl: req.url,
1112
- pathname: '/',
1113
- search: '',
1114
- };
1115
-
1116
- const block = core.protect.inputAnalysis.handleConnect(sourceContext, connectInputs);
1117
-
1118
- expect(scoreRequestConnect).to.have.been.calledOnce;
1119
- expect(core.protect.throwSecurityException).not.to.have.been.called;
1120
- expect(block).to.deep.equal(['block', 'cmd-injection']);
1121
- });
1122
-
1123
- it('handleRequestConnect() does not block when no attack', function () {
1124
- const sourceContext = core.protect.makeSourceContext(req, res);
1125
- sinon.spy(sourceContext, 'block');
1126
-
1127
- const connectInputs = {
1128
- method: req.method,
1129
- headers: req.rawHeaders.map((h, i) => i & 1 ? h : h.toLowerCase()),
1130
- rawUrl: req.url,
1131
- pathname: '/',
1132
- search: '',
1133
- };
1134
-
1135
- srcReturnValue = {
1136
- trackRequest: false,
1137
- };
1138
-
1139
- core.protect.inputAnalysis.handleConnect(sourceContext, connectInputs);
1140
-
1141
- expect(scoreRequestConnect).to.have.been.calledOnce;
1142
- expect(sourceContext.block).not.to.have.been.called;
1143
- });
1144
- });
1145
-
1146
- describe('handlers with real agent-lib', function () {
1147
- let core, al, req, res, reqEmitter, expectedReqData, connectInputs;
1148
-
1149
- beforeEach(function () {
1150
- ({ core } = initProtectFixture());
1151
-
1152
- al = core.protect.agentLib;
1153
- Object.assign(core.config.protect.rules, {
1154
- 'cmd-injection': { mode: 'block' }
1155
- });
1156
-
1157
- // return the real agentLib instantiation.
1158
- sinon.stub(Protect, 'instantiateAgentLib').returns(al);
1159
-
1160
- core.protect = Protect(core);
1161
-
1162
- sinon.spy(core.protect.inputAnalysis, 'handleConnect');
1163
-
1164
- // mock req, res
1165
- reqEmitter = new EventEmitter();
1166
- req = {
1167
- url: '/',
1168
- method: 'GET',
1169
- rawHeaders: ['host', 'vogon.com', 'content-type', 'text/json'],
1170
- // this needs to look sort of like a real incoming message
1171
- emit: (...args) => reqEmitter.emit(...args),
1172
- _events: {},
1173
- socket: { _readableState: { autoDestroy: false } },
1174
- resume: sinon.stub(),
1175
- };
1176
- res = {
1177
- end: sinon.stub(),
1178
- writeHead: sinon.stub(),
1179
- headersSent: false,
1180
- };
1181
-
1182
- // setup a few expected results. these can be modified as needed by
1183
- // different tests.
1184
- expectedReqData = {
1185
- method: req.method,
1186
- headers: req.rawHeaders.map((h, i) => i & 1 ? h : h.toLowerCase()),
1187
- rawUrl: req.url,
1188
- pathname: '/',
1189
- search: '',
1190
- };
1191
- // it's the search string in request parlance but known as query string
1192
- // here.
1193
- connectInputs = Object.assign({}, expectedReqData);
1194
- delete connectInputs.search;
1195
- connectInputs.queries = expectedReqData.search;
1196
- });
1197
-
1198
- describe('handleParsedBody()', function () {
1199
- it('logs at debug level if the body is not an object', function () {
1200
- const ruleId = 'reflected-xss';
1201
- const sourceContext = core.protect.makeSourceContext(req, res);
1202
- sourceContext.policy = {
1203
- rulesMask: al.RuleType[ruleId],
1204
- [ruleId]: 'block',
1205
- };
1206
- const { inputAnalysis } = core.protect;
1207
-
1208
- const parsedBody = 'not-an-object';
1209
- inputAnalysis.handleParsedBody(sourceContext, parsedBody);
1210
-
1211
- const text = 'handleParsedBody() called with non-object';
1212
- expect(core.logger.debug).to.have.been.calledOnceWith({ parsedBody }, text);
1213
- });
1214
-
1215
- it('scores the object as JSON', function () {
1216
- const ruleId = 'nosql-injection-mongo';
1217
- const sourceContext = core.protect.makeSourceContext(req, res);
1218
- sourceContext.policy = {
1219
- ...emptySc.policy,
1220
- rulesMask: al.RuleType[ruleId],
1221
- [ruleId]: 'block',
1222
- };
1223
-
1224
- const body = {
1225
- xyzzy: { $ne: { x: 'y' } }
1226
- };
1227
- const { inputAnalysis } = core.protect;
1228
-
1229
- inputAnalysis.handleParsedBody(sourceContext, body);
1230
- checkNosqlHandleBodyResults(sourceContext, al, body, { mongoContext: true });
1231
- });
1232
-
1233
- it('scores the object as urlencoded', function () {
1234
- const ruleId = 'nosql-injection-mongo';
1235
- const sourceContext = core.protect.makeSourceContext(req, res);
1236
- sourceContext.policy = {
1237
- ...emptySc.policy,
1238
- rulesMask: al.RuleType[ruleId],
1239
- [ruleId]: 'block',
1240
- };
1241
-
1242
- sourceContext.reqData.contentType = 'x-www-form-urlencoded';
1243
- const body = {
1244
- xyzzy: { $ne: { x: 'y' } }
1245
- };
1246
- const { inputAnalysis } = core.protect;
1247
-
1248
- inputAnalysis.handleParsedBody(sourceContext, body);
1249
- const options = { mongoContext: true, type: 'urlencoded' };
1250
- checkNosqlHandleBodyResults(sourceContext, al, body, options);
1251
- });
1252
- });
1253
-
1254
- it('handleRequestEnd works as expected', function () {
1255
- const sourceContext = {
1256
- resultsMap: {
1257
- 'path-traversal': [
1258
- {
1259
- ruleId: 'path-traversal',
1260
- inputType: 'HeaderValue',
1261
- path: [
1262
- 'just-key'
1263
- ],
1264
- key: 'just-key',
1265
- value: 'powerful vector',
1266
- score: 90,
1267
- idsList: [
1268
- '10A-1 & 12'
1269
- ],
1270
- mappedId: 'path-traversal',
1271
- blocked: false,
1272
- exploitMetadata: []
1273
- },
1274
- {
1275
- ruleId: 'path-traversal',
1276
- inputType: 'HeaderValue',
1277
- path: [
1278
- 'hello'
1279
- ],
1280
- key: 'hello',
1281
- value: ';echo put /etc/passwd | tftp host',
1282
- score: 10,
1283
- idsList: [
1284
- '10A-1 & 12'
1285
- ],
1286
- mappedId: 'path-traversal',
1287
- blocked: false,
1288
- exploitMetadata: []
1289
- },
1290
- {
1291
- ruleId: 'path-traversal',
1292
- inputType: 'HeaderKey',
1293
- path: [
1294
- 'hello'
1295
- ],
1296
- key: 'hello',
1297
- value: ';echo put /etc/passwd | tftp host',
1298
- score: 10,
1299
- idsList: [
1300
- '10A-1 & 12'
1301
- ],
1302
- mappedId: 'path-traversal',
1303
- blocked: false,
1304
- exploitMetadata: []
1305
- },
1306
- {
1307
- ruleId: 'path-traversal',
1308
- inputType: 'ParameterValue',
1309
- path: [
1310
- 'hello'
1311
- ],
1312
- key: 'hello',
1313
- value: ';echo put /etc/passwd | tftp host',
1314
- score: 10,
1315
- idsList: [
1316
- '10A-1 & 12'
1317
- ],
1318
- mappedId: 'path-traversal',
1319
- blocked: false,
1320
- exploitMetadata: []
1321
- },
1322
- {
1323
- ruleId: 'path-traversal',
1324
- inputType: 'CookieValue',
1325
- path: [
1326
- 'hello'
1327
- ],
1328
- key: 'hello',
1329
- value: ';echo put /etc/passwd | tftp host',
1330
- score: 10,
1331
- idsList: [
1332
- '10A-1 & 12'
1333
- ],
1334
- mappedId: 'path-traversal',
1335
- blocked: false,
1336
- exploitMetadata: []
1337
- }
1338
- ],
1339
- 'sql-injection': [
1340
- {
1341
- ruleId: 'sql-injection',
1342
- inputType: 'HeaderValue',
1343
- path: [
1344
- 'hello'
1345
- ],
1346
- key: 'hello',
1347
- value: ';echo put /etc/passwd | tftp host',
1348
- score: 10,
1349
- idsList: [],
1350
- mappedId: 'sql-injection',
1351
- blocked: false,
1352
- exploitMetadata: []
1353
- },
1354
- {
1355
- ruleId: 'sql-injection',
1356
- inputType: 'HeaderValue',
1357
- path: [
1358
- 'postman-token'
1359
- ],
1360
- key: 'postman-token',
1361
- value: '8f0ee1c6-81e1-4a2e-93fe-1337907ccc6f',
1362
- score: 10,
1363
- idsList: [],
1364
- mappedId: 'sql-injection',
1365
- blocked: false,
1366
- exploitMetadata: []
1367
- },
1368
- {
1369
- ruleId: 'sql-injection',
1370
- inputType: 'CookieValue',
1371
- path: [
1372
- 'hello'
1373
- ],
1374
- key: 'hello',
1375
- value: ';echo put /etc/passwd | tftp host',
1376
- score: 10,
1377
- idsList: [
1378
- '10A-1 & 12'
1379
- ],
1380
- mappedId: 'path-traversal',
1381
- blocked: false,
1382
- exploitMetadata: []
1383
- }
1384
- ],
1385
- 'cmd-injection': [
1386
- {
1387
- ruleId: 'cmd-injection',
1388
- inputType: 'CookieValue',
1389
- path: [
1390
- 'hello'
1391
- ],
1392
- key: 'hello',
1393
- value: 'test&whoami',
1394
- score: 10,
1395
- idsList: [
1396
- '10A-1 & 12'
1397
- ],
1398
- mappedId: 'cmd-injection',
1399
- blocked: false,
1400
- exploitMetadata: []
1401
- }
1402
- ],
1403
- },
1404
- policy: {
1405
- rulesMask: 511,
1406
- [Rule.PATH_TRAVERSAL]: ProtectRuleMode.MONITOR,
1407
- [Rule.SQL_INJECTION]: ProtectRuleMode.MONITOR,
1408
- [Rule.CMD_INJECTION]: ProtectRuleMode.BLOCK
1409
- }
1410
- };
1411
-
1412
- core.protect.inputAnalysis.handleRequestEnd(sourceContext, connectInputs);
1413
-
1414
- expect(sourceContext.resultsMap[Rule.PATH_TRAVERSAL]).to.eql([
1415
- {
1416
- ruleId: 'path-traversal',
1417
- inputType: 'HeaderValue',
1418
- path: [
1419
- 'just-key'
1420
- ],
1421
- key: 'just-key',
1422
- value: 'powerful vector',
1423
- score: 90,
1424
- idsList: [
1425
- '10A-1 & 12'
1426
- ],
1427
- mappedId: 'path-traversal',
1428
- blocked: false,
1429
- exploitMetadata: []
1430
- },
1431
- {
1432
- ruleId: 'path-traversal',
1433
- inputType: 'HeaderValue',
1434
- path: [
1435
- 'hello'
1436
- ],
1437
- key: 'hello',
1438
- value: ';echo put /etc/passwd | tftp host',
1439
- score: 10,
1440
- idsList: [
1441
- '10A-1 & 12'
1442
- ],
1443
- mappedId: 'path-traversal',
1444
- blocked: false,
1445
- exploitMetadata: []
1446
- },
1447
- {
1448
- ruleId: 'path-traversal',
1449
- inputType: 'HeaderKey',
1450
- path: [
1451
- 'hello'
1452
- ],
1453
- key: 'hello',
1454
- value: ';echo put /etc/passwd | tftp host',
1455
- score: 10,
1456
- idsList: [
1457
- '10A-1 & 12'
1458
- ],
1459
- mappedId: 'path-traversal',
1460
- blocked: false,
1461
- exploitMetadata: []
1462
- },
1463
- {
1464
- ruleId: 'path-traversal',
1465
- inputType: 'ParameterValue',
1466
- path: [
1467
- 'hello'
1468
- ],
1469
- key: 'hello',
1470
- value: ';echo put /etc/passwd | tftp host',
1471
- score: 10,
1472
- idsList: [
1473
- '10A-1 & 12'
1474
- ],
1475
- mappedId: 'path-traversal',
1476
- blocked: false,
1477
- exploitMetadata: []
1478
- },
1479
- {
1480
- ruleId: 'path-traversal',
1481
- inputType: 'CookieValue',
1482
- path: [
1483
- 'hello'
1484
- ],
1485
- key: 'hello',
1486
- value: ';echo put /etc/passwd | tftp host',
1487
- score: 10,
1488
- idsList: [
1489
- '10A-1 & 12'
1490
- ],
1491
- mappedId: 'path-traversal',
1492
- blocked: false,
1493
- exploitMetadata: []
1494
- },
1495
- {
1496
- ruleId: 'path-traversal',
1497
- inputType: 'ParameterValue',
1498
- path: [
1499
- 'hello'
1500
- ],
1501
- key: 'hello',
1502
- value: ';echo put /etc/passwd | tftp host',
1503
- score: 90,
1504
- idsList: [
1505
- '10A-1-0',
1506
- '3000-12'
1507
- ],
1508
- mappedId: 'path-traversal',
1509
- blocked: false,
1510
- exploitMetadata: []
1511
- },
1512
- {
1513
- ruleId: 'path-traversal',
1514
- inputType: 'HeaderValue',
1515
- path: [
1516
- 'hello'
1517
- ],
1518
- key: 'hello',
1519
- value: ';echo put /etc/passwd | tftp host',
1520
- score: 90,
1521
- idsList: [
1522
- '10A-1-0',
1523
- '3000-12'
1524
- ],
1525
- mappedId: 'path-traversal',
1526
- blocked: false,
1527
- exploitMetadata: []
1528
- },
1529
- {
1530
- ruleId: 'path-traversal',
1531
- inputType: 'CookieValue',
1532
- path: [
1533
- 'hello'
1534
- ],
1535
- key: 'hello',
1536
- value: ';echo put /etc/passwd | tftp host',
1537
- score: 90,
1538
- idsList: [
1539
- '10A-1-0',
1540
- '3000-12'
1541
- ],
1542
- mappedId: 'path-traversal',
1543
- blocked: false,
1544
- exploitMetadata: []
1545
- },
1546
- {
1547
- ruleId: 'path-traversal',
1548
- inputType: 'HeaderKey',
1549
- path: [
1550
- 'hello'
1551
- ],
1552
- key: 'hello',
1553
- value: ';echo put /etc/passwd | tftp host',
1554
- score: 90,
1555
- idsList: [
1556
- '10A-1-0',
1557
- '3000-12'
1558
- ],
1559
- mappedId: 'path-traversal',
1560
- blocked: false,
1561
- exploitMetadata: []
1562
- }
1563
- ]);
1564
- });
1565
- });
1566
- });
1567
-
1568
- function checkNosqlHandleBodyResults(sourceContext, agentLib, body, opts = {}) {
1569
- let keyType = 'JsonKey';
1570
- let bodyType = 'json';
1571
- if (opts.type === 'urlencoded') {
1572
- keyType = 'ParameterKey';
1573
- bodyType = 'urlencoded';
1574
- }
1575
- let obj = body;
1576
- const path = [];
1577
- let next = Object.keys(body)[0];
1578
- while (next !== '$ne' && next !== '$finalize') {
1579
- path.push(next);
1580
- obj = obj[next];
1581
- next = Object.keys(obj)[0];
1582
- }
1583
- const inputToCheck = obj[next];
1584
-
1585
- expect(sourceContext.trackRequest).to.be.true;
1586
- expect(sourceContext.securityException).to.be.undefined;
1587
- expect(sourceContext.bodyType).to.equal(bodyType);
1588
- expect(sourceContext.resultsMap).an('object').keys('nosql-injection-mongo');
1589
-
1590
- const nosql = sourceContext.resultsMap['nosql-injection-mongo'];
1591
- expect(nosql).an('array').length(1);
1592
- expect(nosql[0].ruleId).to.equal('nosql-injection-mongo');
1593
- expect(nosql[0].inputType).to.equal(keyType);
1594
- expect(nosql[0].path).an('array').to.deep.equal(path);
1595
- expect(nosql[0].value).to.equal('$ne');
1596
- expect(nosql[0].score).to.equal(10);
1597
- expect(nosql[0].mappedId).to.equal('nosql-injection');
1598
- expect(nosql[0].blocked).to.be.false;
1599
- expect(nosql[0].exploitMetadata).to.deep.equal([]);
1600
-
1601
- if (opts.mongoContext) {
1602
- expect(nosql[0].mongoContext).an('object').keys('inputToCheck').to.deep.equal({ inputToCheck });
1603
- }
1604
- }