@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.
- package/LICENSE +12 -0
- package/README.md +9 -0
- package/lib/cli-rewriter.js +20 -0
- package/lib/error-handlers/constants.js +5 -0
- package/lib/error-handlers/index.js +13 -0
- package/lib/error-handlers/install/fastify3.js +88 -0
- package/lib/error-handlers/install/fastify3.test.js +142 -0
- package/lib/esm-loader.mjs +2 -0
- package/lib/esm-loader.test.mjs +11 -0
- package/lib/index.d.ts +36 -0
- package/lib/index.js +89 -0
- package/lib/index.test.js +32 -0
- package/lib/input-analysis/handlers.js +462 -0
- package/lib/input-analysis/handlers.test.js +898 -0
- package/lib/input-analysis/index.js +16 -0
- package/lib/input-analysis/index.test.js +28 -0
- package/lib/input-analysis/install/fastify3.js +79 -0
- package/lib/input-analysis/install/fastify3.test.js +71 -0
- package/lib/input-analysis/install/http.js +185 -0
- package/lib/input-analysis/install/http.test.js +315 -0
- package/lib/input-tracing/constants.js +5 -0
- package/lib/input-tracing/handlers/index.js +117 -0
- package/lib/input-tracing/handlers/index.test.js +395 -0
- package/lib/input-tracing/handlers/nosql-injection-mongo.js +48 -0
- package/lib/input-tracing/index.js +32 -0
- package/lib/input-tracing/install/README.md +1 -0
- package/lib/input-tracing/install/child-process.js +45 -0
- package/lib/input-tracing/install/child-process.test.js +112 -0
- package/lib/input-tracing/install/fs.js +107 -0
- package/lib/input-tracing/install/fs.test.js +118 -0
- package/lib/input-tracing/install/mysql.js +57 -0
- package/lib/input-tracing/install/mysql.test.js +108 -0
- package/lib/input-tracing/install/postgres.js +61 -0
- package/lib/input-tracing/install/postgres.test.js +125 -0
- package/lib/input-tracing/install/sequelize.js +51 -0
- package/lib/input-tracing/install/sequelize.test.js +79 -0
- package/lib/input-tracing/install/sqlite3.js +45 -0
- package/lib/input-tracing/install/sqlite3.test.js +88 -0
- package/lib/make-response-blocker.js +35 -0
- package/lib/make-response-blocker.test.js +88 -0
- package/lib/make-source-context.js +130 -0
- package/lib/make-source-context.test.js +298 -0
- package/lib/security-exception.js +12 -0
- package/lib/throw-security-exception.js +30 -0
- package/lib/throw-security-exception.test.js +50 -0
- package/lib/utils.js +88 -0
- package/lib/utils.test.js +40 -0
- 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
|
+
}
|