@contrast/protect 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/error-handlers/index.js +7 -4
- package/lib/error-handlers/install/express4.js +2 -3
- package/lib/error-handlers/install/{fastify3.js → fastify.js} +13 -15
- package/lib/error-handlers/install/hapi.js +75 -0
- package/lib/error-handlers/install/koa2.js +1 -2
- package/lib/{cli-rewriter.js → get-source-context.js} +13 -15
- package/lib/hardening/constants.js +20 -0
- package/lib/hardening/handlers.js +65 -0
- package/lib/hardening/index.js +29 -0
- package/lib/hardening/install/node-serialize0.js +59 -0
- package/lib/index.d.ts +3 -21
- package/lib/index.js +6 -46
- package/lib/input-analysis/handlers.js +198 -39
- package/lib/input-analysis/index.js +9 -6
- package/lib/input-analysis/install/body-parser1.js +20 -18
- package/lib/input-analysis/install/cookie-parser1.js +13 -15
- package/lib/input-analysis/install/express4.js +8 -13
- package/lib/input-analysis/install/fastify.js +86 -0
- package/lib/input-analysis/install/formidable1.js +4 -5
- package/lib/input-analysis/install/hapi.js +106 -0
- package/lib/input-analysis/install/http.js +80 -30
- package/lib/input-analysis/install/koa-body5.js +5 -10
- package/lib/input-analysis/install/koa-bodyparser4.js +6 -10
- package/lib/input-analysis/install/koa2.js +13 -24
- package/lib/input-analysis/install/multer1.js +5 -6
- package/lib/input-analysis/install/qs6.js +7 -11
- package/lib/input-analysis/install/universal-cookie4.js +3 -7
- package/lib/input-analysis/ip-analysis.js +76 -0
- package/lib/input-analysis/virtual-patches.js +109 -0
- package/lib/input-tracing/handlers/index.js +92 -23
- package/lib/input-tracing/index.js +14 -18
- package/lib/input-tracing/install/child-process.js +13 -7
- package/lib/input-tracing/install/eval.js +60 -0
- package/lib/input-tracing/install/fs.js +4 -2
- package/lib/input-tracing/install/function.js +60 -0
- package/lib/input-tracing/install/http.js +63 -0
- package/lib/input-tracing/install/mongodb.js +20 -20
- package/lib/input-tracing/install/mysql.js +3 -2
- package/lib/input-tracing/install/postgres.js +5 -4
- package/lib/input-tracing/install/sequelize.js +7 -5
- package/lib/input-tracing/install/sqlite3.js +6 -4
- package/lib/input-tracing/install/vm.js +132 -0
- package/lib/make-source-context.js +8 -49
- package/lib/policy.js +134 -0
- package/lib/semantic-analysis/handlers.js +161 -0
- package/lib/semantic-analysis/index.js +38 -0
- package/package.json +7 -9
- package/lib/input-analysis/install/fastify3.js +0 -107
- package/lib/utils.js +0 -84
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
|
-
const { simpleTraverse } = require('
|
|
18
|
+
const { BLOCKING_MODES, simpleTraverse } = require('@contrast/common');
|
|
19
|
+
const address = require('ipaddr.js');
|
|
19
20
|
|
|
20
21
|
//
|
|
21
22
|
// these rules are not implemented by agent-lib, but are being considered for
|
|
@@ -110,13 +111,17 @@ module.exports = function(core) {
|
|
|
110
111
|
* @returns {undefined|[String]} undefined to permit else [mode, rule] to block.
|
|
111
112
|
*/
|
|
112
113
|
inputAnalysis.handleConnect = function handleConnect(sourceContext, connectInputs) {
|
|
113
|
-
|
|
114
|
+
if (!sourceContext || sourceContext.allowed) return;
|
|
115
|
+
|
|
116
|
+
const { policy: { rulesMask } } = sourceContext;
|
|
117
|
+
|
|
118
|
+
inputAnalysis.handleVirtualPatches(sourceContext, { URLS: connectInputs.rawUrl, HEADERS: connectInputs.headers });
|
|
114
119
|
|
|
115
120
|
// initialize findings to the basics
|
|
116
121
|
let block = undefined;
|
|
117
|
-
if (
|
|
118
|
-
const findings = agentLib.scoreRequestConnect(
|
|
119
|
-
block = mergeFindings(
|
|
122
|
+
if (rulesMask !== 0) {
|
|
123
|
+
const findings = agentLib.scoreRequestConnect(rulesMask, connectInputs, preferWW);
|
|
124
|
+
block = mergeFindings(sourceContext, findings);
|
|
120
125
|
}
|
|
121
126
|
|
|
122
127
|
return block;
|
|
@@ -130,12 +135,12 @@ module.exports = function(core) {
|
|
|
130
135
|
throw new Error('nyi', sourceContext);
|
|
131
136
|
};
|
|
132
137
|
|
|
133
|
-
|
|
134
138
|
const jsonInputTypes = {
|
|
135
|
-
keyType: agentLib.InputType.JsonKey,
|
|
139
|
+
keyType: agentLib.InputType.JsonKey, inputType: agentLib.InputType.JsonValue
|
|
136
140
|
};
|
|
141
|
+
|
|
137
142
|
const parameterInputTypes = {
|
|
138
|
-
keyType: agentLib.InputType.ParameterKey,
|
|
143
|
+
keyType: agentLib.InputType.ParameterKey, inputType: agentLib.InputType.ParameterValue
|
|
139
144
|
};
|
|
140
145
|
|
|
141
146
|
/**
|
|
@@ -155,10 +160,17 @@ module.exports = function(core) {
|
|
|
155
160
|
* @param {Object} queryParams pojo {key: value, ...} for all query params/search params
|
|
156
161
|
*/
|
|
157
162
|
inputAnalysis.handleQueryParams = function handleQueryParams(sourceContext, queryParams) {
|
|
163
|
+
if (sourceContext.analyzedQuery) return;
|
|
164
|
+
sourceContext.analyzedQuery = true;
|
|
165
|
+
|
|
158
166
|
if (typeof queryParams !== 'object') {
|
|
159
167
|
logger.debug({ queryParams }, 'handleQueryParams() called with non-object');
|
|
160
168
|
return;
|
|
161
169
|
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
inputAnalysis.handleVirtualPatches(sourceContext, { PARAMETERS: queryParams });
|
|
173
|
+
|
|
162
174
|
const block = commonObjectAnalyzer(sourceContext, queryParams, parameterInputTypes);
|
|
163
175
|
|
|
164
176
|
if (block) {
|
|
@@ -176,12 +188,17 @@ module.exports = function(core) {
|
|
|
176
188
|
* @param {Object} urlParams pojo
|
|
177
189
|
*/
|
|
178
190
|
inputAnalysis.handleUrlParams = function(sourceContext, urlParams) {
|
|
191
|
+
if (sourceContext.analyzedUrlParams) return;
|
|
192
|
+
sourceContext.analyzedUrlParams = true;
|
|
193
|
+
|
|
179
194
|
if (typeof urlParams !== 'object') {
|
|
180
195
|
logger.debug({ urlParams }, 'handleUrlParams() called with non-object');
|
|
181
196
|
return;
|
|
182
197
|
}
|
|
183
198
|
|
|
184
|
-
|
|
199
|
+
inputAnalysis.handleVirtualPatches(sourceContext, { PARAMETERS: urlParams });
|
|
200
|
+
|
|
201
|
+
const { policy: { rulesMask } } = sourceContext;
|
|
185
202
|
const resultsList = [];
|
|
186
203
|
const { UrlParameter } = agentLib.InputType;
|
|
187
204
|
|
|
@@ -190,7 +207,7 @@ module.exports = function(core) {
|
|
|
190
207
|
if (type !== 'Value') {
|
|
191
208
|
return;
|
|
192
209
|
}
|
|
193
|
-
const items = agentLib.scoreAtom(
|
|
210
|
+
const items = agentLib.scoreAtom(rulesMask, value, UrlParameter, preferWW);
|
|
194
211
|
if (!items) {
|
|
195
212
|
return;
|
|
196
213
|
}
|
|
@@ -219,7 +236,7 @@ module.exports = function(core) {
|
|
|
219
236
|
resultsList,
|
|
220
237
|
};
|
|
221
238
|
|
|
222
|
-
const block = mergeFindings(
|
|
239
|
+
const block = mergeFindings(sourceContext, urlParamsFindings);
|
|
223
240
|
|
|
224
241
|
if (block) {
|
|
225
242
|
core.protect.throwSecurityException(sourceContext);
|
|
@@ -235,14 +252,20 @@ module.exports = function(core) {
|
|
|
235
252
|
* @param {Object} cookies pojo
|
|
236
253
|
*/
|
|
237
254
|
inputAnalysis.handleCookies = function(sourceContext, cookies) {
|
|
255
|
+
if (sourceContext.analyzedCookies) return;
|
|
256
|
+
sourceContext.analyzedCookies = true;
|
|
257
|
+
|
|
238
258
|
const cookiesArr = Object.entries(cookies).reduce((acc, [key, value]) => {
|
|
239
259
|
acc.push(key, value);
|
|
240
260
|
return acc;
|
|
241
261
|
}, []);
|
|
242
|
-
const { rules, rules: { agentLibRulesMask: mask } } = sourceContext;
|
|
243
|
-
const cookieFindings = agentLib.scoreRequestConnect(mask, { cookies: cookiesArr }, preferWW);
|
|
244
262
|
|
|
245
|
-
|
|
263
|
+
inputAnalysis.handleVirtualPatches(sourceContext, { HEADERS: cookies });
|
|
264
|
+
|
|
265
|
+
const { policy: { rulesMask } } = sourceContext;
|
|
266
|
+
const cookieFindings = agentLib.scoreRequestConnect(rulesMask, { cookies: cookiesArr }, preferWW);
|
|
267
|
+
|
|
268
|
+
const block = mergeFindings(sourceContext, cookieFindings);
|
|
246
269
|
|
|
247
270
|
if (block) {
|
|
248
271
|
core.protect.throwSecurityException(sourceContext);
|
|
@@ -259,11 +282,16 @@ module.exports = function(core) {
|
|
|
259
282
|
* @param {Object} parsedBody
|
|
260
283
|
*/
|
|
261
284
|
inputAnalysis.handleParsedBody = function(sourceContext, parsedBody) {
|
|
285
|
+
if (sourceContext.analyzedBody) return;
|
|
286
|
+
sourceContext.analyzedBody = true;
|
|
287
|
+
|
|
262
288
|
if (typeof parsedBody !== 'object') {
|
|
263
289
|
logger.debug({ parsedBody }, 'handleParsedBody() called with non-object');
|
|
264
290
|
return;
|
|
265
291
|
}
|
|
266
292
|
|
|
293
|
+
inputAnalysis.handleVirtualPatches(sourceContext, { PARAMETERS: parsedBody });
|
|
294
|
+
|
|
267
295
|
let bodyType;
|
|
268
296
|
let inputTypes;
|
|
269
297
|
if (sourceContext.reqData.contentType.includes('/json')) {
|
|
@@ -288,6 +316,70 @@ module.exports = function(core) {
|
|
|
288
316
|
throw new Error('nyi', sourceContext, name);
|
|
289
317
|
};
|
|
290
318
|
|
|
319
|
+
inputAnalysis.handleVirtualPatches = function(sourceContext, requestInput) {
|
|
320
|
+
const ruleId = 'virtual-patch';
|
|
321
|
+
|
|
322
|
+
if (!Object.keys(requestInput).filter(Boolean).length || !sourceContext?.virtualPatchesEvaluators.length) return;
|
|
323
|
+
|
|
324
|
+
for (const vpEvaluators of sourceContext.virtualPatchesEvaluators) {
|
|
325
|
+
for (const key in requestInput) {
|
|
326
|
+
const evaluator = vpEvaluators.get(key);
|
|
327
|
+
|
|
328
|
+
if (evaluator && requestInput[key] && evaluator(requestInput[key])) {
|
|
329
|
+
vpEvaluators.delete(key);
|
|
330
|
+
const { name, uuid } = vpEvaluators.get('metadata');
|
|
331
|
+
|
|
332
|
+
if (vpEvaluators.size === 1 && uuid) {
|
|
333
|
+
if (!sourceContext.findings.serverFeaturesResultsMap[ruleId]) {
|
|
334
|
+
sourceContext.findings.serverFeaturesResultsMap[ruleId] = [];
|
|
335
|
+
}
|
|
336
|
+
sourceContext.findings.serverFeaturesResultsMap[ruleId].push({
|
|
337
|
+
name,
|
|
338
|
+
uuid
|
|
339
|
+
});
|
|
340
|
+
sourceContext.findings.securityException = ['block', ruleId];
|
|
341
|
+
core.protect.throwSecurityException(sourceContext);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
inputAnalysis.handleIpAllowlist = function(sourceContext, ipAllowlist) {
|
|
349
|
+
if (!sourceContext || !ipAllowlist.length) return;
|
|
350
|
+
|
|
351
|
+
const { ip: reqIp, headers: reqHeaders } = sourceContext.reqData;
|
|
352
|
+
|
|
353
|
+
const match = ipListAnalysis(reqIp, reqHeaders, ipAllowlist);
|
|
354
|
+
|
|
355
|
+
if (match) {
|
|
356
|
+
logger.info(match, 'Found a matching IP to an entry in ipAllow list');
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
inputAnalysis.handleIpDenylist = function(sourceContext, ipDenylist) {
|
|
362
|
+
const ruleId = 'ip-denylist';
|
|
363
|
+
|
|
364
|
+
if (!sourceContext || !ipDenylist.length) return;
|
|
365
|
+
|
|
366
|
+
const { ip: reqIp, headers: reqHeaders } = sourceContext.reqData;
|
|
367
|
+
|
|
368
|
+
const match = ipListAnalysis(reqIp, reqHeaders, ipDenylist);
|
|
369
|
+
|
|
370
|
+
if (match) {
|
|
371
|
+
logger.info(match, 'Found a matching IP to an entry in ipDeny list');
|
|
372
|
+
if (!sourceContext.findings.serverFeaturesResultsMap[ruleId]) {
|
|
373
|
+
sourceContext.findings.serverFeaturesResultsMap[ruleId] = [];
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
sourceContext.findings.serverFeaturesResultsMap[ruleId].push({
|
|
377
|
+
ip: match.matchedIp,
|
|
378
|
+
uuid: match.uuid,
|
|
379
|
+
});
|
|
380
|
+
return ['block', 'ip-denylist'];
|
|
381
|
+
}
|
|
382
|
+
};
|
|
291
383
|
|
|
292
384
|
/**
|
|
293
385
|
* commonObjectAnalyzer() walks an object supplied by the end-user and checks
|
|
@@ -305,11 +397,12 @@ module.exports = function(core) {
|
|
|
305
397
|
* @returns {Array | undefined} returns an array with block info if vulnerability was found.
|
|
306
398
|
*/
|
|
307
399
|
function commonObjectAnalyzer(sourceContext, object, inputTypes) {
|
|
308
|
-
const {
|
|
400
|
+
const { policy: { rulesMask } } = sourceContext;
|
|
401
|
+
if (!rulesMask) return;
|
|
402
|
+
|
|
309
403
|
// use inputTypes to set params...
|
|
310
|
-
const { keyType,
|
|
404
|
+
const { keyType, inputType } = inputTypes;
|
|
311
405
|
const inputTypeStr = inputTypes === jsonInputTypes ? 'Json' : 'Parameter';
|
|
312
|
-
const { Where } = agentLib.MongoQueryType;
|
|
313
406
|
const resultsList = [];
|
|
314
407
|
|
|
315
408
|
// it's possible to optimize this if qs (or a similar package) is not loaded
|
|
@@ -328,7 +421,7 @@ module.exports = function(core) {
|
|
|
328
421
|
/* eslint-disable-next-line complexity */
|
|
329
422
|
simpleTraverse(object, function(path, type, value) {
|
|
330
423
|
let itemType;
|
|
331
|
-
let
|
|
424
|
+
let isMongoQueryType;
|
|
332
425
|
// this is a bit awkward now because nosql-injection-mongo is not integrated
|
|
333
426
|
// into the scoreAtom() function (or the check_input() function it uses). as
|
|
334
427
|
// a result, the two rules need to be checked independently and the results
|
|
@@ -336,14 +429,14 @@ module.exports = function(core) {
|
|
|
336
429
|
// TODO AGENT-205
|
|
337
430
|
if (type === 'Key') {
|
|
338
431
|
itemType = keyType;
|
|
339
|
-
if (
|
|
340
|
-
|
|
432
|
+
if (rulesMask & agentLib.RuleType['nosql-injection-mongo']) {
|
|
433
|
+
isMongoQueryType = agentLib.isMongoQueryType(value);
|
|
341
434
|
}
|
|
342
435
|
} else {
|
|
343
|
-
itemType =
|
|
436
|
+
itemType = inputType;
|
|
344
437
|
}
|
|
345
|
-
let items = agentLib.scoreAtom(
|
|
346
|
-
if (!items && !
|
|
438
|
+
let items = agentLib.scoreAtom(rulesMask, value, itemType, preferWW);
|
|
439
|
+
if (!items && !isMongoQueryType) {
|
|
347
440
|
return;
|
|
348
441
|
}
|
|
349
442
|
if (!items) {
|
|
@@ -352,18 +445,13 @@ module.exports = function(core) {
|
|
|
352
445
|
let mongoPath;
|
|
353
446
|
// if the key was a mongo query key, then add it to the items. it requires
|
|
354
447
|
// that additional information is kept as well.
|
|
355
|
-
if (
|
|
448
|
+
if (isMongoQueryType) {
|
|
356
449
|
const inputToCheck = getValueAtKey(object, path, value);
|
|
357
450
|
// because scoreRequestConnect() returns the query type in the value, we
|
|
358
451
|
// mimic it here (where scoreAtom() was used). the actual object/string
|
|
359
452
|
// to match is stored as `inputToCheck`.
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
if (mongoQueryType <= Where || inputType === 'string') {
|
|
363
|
-
// the query-type/input-type combination is valid. add a synthesized item.
|
|
364
|
-
const item = { ruleId: 'nosql-injection-mongo', score: 10, mongoContext: { inputToCheck } };
|
|
365
|
-
items.push(item);
|
|
366
|
-
}
|
|
453
|
+
const item = { ruleId: 'nosql-injection-mongo', score: 10, mongoContext: { inputToCheck } };
|
|
454
|
+
items.push(item);
|
|
367
455
|
}
|
|
368
456
|
// make each item a complete Finding
|
|
369
457
|
for (const item of items) {
|
|
@@ -372,8 +460,7 @@ module.exports = function(core) {
|
|
|
372
460
|
inputType: `${inputTypeStr}${type}`,
|
|
373
461
|
path: mongoPath || path.slice(),
|
|
374
462
|
key: type === 'Key' ? value : path[path.length - 1],
|
|
375
|
-
|
|
376
|
-
value: mongoQueryType || value,
|
|
463
|
+
value,
|
|
377
464
|
score: item.score,
|
|
378
465
|
idsList: [],
|
|
379
466
|
};
|
|
@@ -396,7 +483,51 @@ module.exports = function(core) {
|
|
|
396
483
|
resultsList,
|
|
397
484
|
};
|
|
398
485
|
|
|
399
|
-
return mergeFindings(
|
|
486
|
+
return mergeFindings(sourceContext, findings);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function ipListAnalysis(reqIp, reqHeaders, list) {
|
|
490
|
+
const forwardedIps = [];
|
|
491
|
+
|
|
492
|
+
for (let i = 0; i < reqHeaders.length; i++) {
|
|
493
|
+
if (reqHeaders[i] === 'x-forwarded-for') {
|
|
494
|
+
const ipsFromHeaders = reqHeaders[i + 1]?.split(/[,;]+/);
|
|
495
|
+
forwardedIps.push(...ipsFromHeaders);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const ipsToCheck = [reqIp, ...forwardedIps];
|
|
500
|
+
const now = new Date().getTime();
|
|
501
|
+
|
|
502
|
+
/* c8 ignore next 3 */
|
|
503
|
+
if (!ipsToCheck.length) {
|
|
504
|
+
return false;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
for (const listEntry of list) {
|
|
508
|
+
const { doesExpire, expiresAt } = listEntry;
|
|
509
|
+
for (let i = 0; i < ipsToCheck.length; i++) {
|
|
510
|
+
const currentIp = ipsToCheck[i];
|
|
511
|
+
|
|
512
|
+
// Ignore bad IP values.
|
|
513
|
+
if (!address.isValid(currentIp)) {
|
|
514
|
+
logger.warn(`Unable to parse ${currentIp}.`);
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
const expired = doesExpire ? expiresAt - now <= 0 : false;
|
|
518
|
+
|
|
519
|
+
if (expired) {
|
|
520
|
+
logger.info(`IP expired: ${listEntry.name}, ${listEntry.ip}`);
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const match = checkIpsMatch(listEntry, currentIp);
|
|
525
|
+
|
|
526
|
+
if (match) {
|
|
527
|
+
return match;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
400
531
|
}
|
|
401
532
|
};
|
|
402
533
|
|
|
@@ -407,11 +538,13 @@ module.exports = function(core) {
|
|
|
407
538
|
* @param {Object} newFindings the findings, in {trackRequest, resultsList} format.
|
|
408
539
|
* @returns {undefined|[String]} undefined to permit else [mode, rule] to block.
|
|
409
540
|
*/
|
|
410
|
-
function mergeFindings(
|
|
541
|
+
function mergeFindings(sourceContext, newFindings) {
|
|
542
|
+
const { findings, policy } = sourceContext;
|
|
543
|
+
|
|
411
544
|
if (!newFindings.trackRequest) {
|
|
412
545
|
return findings.securityException;
|
|
413
546
|
}
|
|
414
|
-
normalizeFindings(
|
|
547
|
+
normalizeFindings(policy, newFindings);
|
|
415
548
|
|
|
416
549
|
findings.trackRequest = findings.trackRequest || newFindings.trackRequest;
|
|
417
550
|
findings.securityException = findings.securityException || newFindings.securityException;
|
|
@@ -430,7 +563,7 @@ function mergeFindings(rules, findings, newFindings) {
|
|
|
430
563
|
//
|
|
431
564
|
// add common fields to findings.
|
|
432
565
|
//
|
|
433
|
-
function normalizeFindings(
|
|
566
|
+
function normalizeFindings(policy, findings) {
|
|
434
567
|
// now both augment the rules and check to see if any require blocking
|
|
435
568
|
// at perimeter.
|
|
436
569
|
for (const r of findings.resultsList) {
|
|
@@ -454,14 +587,39 @@ function normalizeFindings(rules, findings) {
|
|
|
454
587
|
// option and that implies that there is no sink, so this is the only place at
|
|
455
588
|
// which the block can occur. so at a minimum 'block' should also result in a
|
|
456
589
|
// block.
|
|
457
|
-
const
|
|
458
|
-
if (r.score >= 90 &&
|
|
590
|
+
const mode = policy[r.ruleId];
|
|
591
|
+
if (r.score >= 90 && BLOCKING_MODES.includes(mode)) {
|
|
459
592
|
r.blocked = true;
|
|
460
593
|
findings.securityException = [mode, r.ruleId];
|
|
461
594
|
}
|
|
462
595
|
}
|
|
463
596
|
}
|
|
464
597
|
|
|
598
|
+
|
|
599
|
+
function checkIpsMatch(listEntry, ip) {
|
|
600
|
+
const parsed = address.process(ip);
|
|
601
|
+
|
|
602
|
+
// Check if IP is in CIDR range,
|
|
603
|
+
if (listEntry.cidr) {
|
|
604
|
+
if (parsed.kind() !== listEntry.cidr.kind) {
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if (parsed.match(listEntry.cidr.range)) {
|
|
609
|
+
return { ...listEntry, match: ip };
|
|
610
|
+
} else {
|
|
611
|
+
return null;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// or do a direct comparison
|
|
616
|
+
if (parsed.toNormalizedString() === listEntry.normalizedValue) {
|
|
617
|
+
return { ...listEntry, matchedIp: ip };
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
622
|
+
|
|
465
623
|
/**
|
|
466
624
|
* getValueAtKey() is used to fetch the object (expected) associated
|
|
467
625
|
* with the path of keys in obj. i say expected because this is only used
|
|
@@ -476,6 +634,7 @@ function normalizeFindings(rules, findings) {
|
|
|
476
634
|
*/
|
|
477
635
|
function getValueAtKey(obj, path, key) {
|
|
478
636
|
for (const p of path) {
|
|
637
|
+
/* c8 ignore next 6 */
|
|
479
638
|
if (!(p in obj)) {
|
|
480
639
|
return undefined;
|
|
481
640
|
}
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
|
+
const { installChildComponentsSync } = require('@contrast/common');
|
|
19
|
+
|
|
18
20
|
module.exports = function(core) {
|
|
19
21
|
const inputAnalysis = core.protect.inputAnalysis = {};
|
|
20
22
|
|
|
@@ -35,16 +37,17 @@ module.exports = function(core) {
|
|
|
35
37
|
require('./install/universal-cookie4')(core);
|
|
36
38
|
|
|
37
39
|
// framework specific instrumentation
|
|
38
|
-
require('./install/
|
|
40
|
+
require('./install/fastify')(core);
|
|
39
41
|
require('./install/koa2')(core);
|
|
40
42
|
require('./install/express4')(core);
|
|
43
|
+
require('./install/hapi')(core);
|
|
44
|
+
|
|
45
|
+
// virtual patches
|
|
46
|
+
require('./virtual-patches')(core);
|
|
47
|
+
require('./ip-analysis')(core);
|
|
41
48
|
|
|
42
49
|
inputAnalysis.install = function() {
|
|
43
|
-
|
|
44
|
-
.filter((property) => property.install)
|
|
45
|
-
.forEach((library) => {
|
|
46
|
-
library.install();
|
|
47
|
-
});
|
|
50
|
+
installChildComponentsSync(inputAnalysis);
|
|
48
51
|
};
|
|
49
52
|
|
|
50
53
|
return inputAnalysis;
|
|
@@ -21,29 +21,35 @@ module.exports = (core) => {
|
|
|
21
21
|
const {
|
|
22
22
|
depHooks,
|
|
23
23
|
logger,
|
|
24
|
-
|
|
24
|
+
protect,
|
|
25
25
|
protect: { inputAnalysis },
|
|
26
26
|
} = core;
|
|
27
27
|
|
|
28
28
|
function contrastNext(req, origNext, fnName) {
|
|
29
29
|
return function next(origErr) {
|
|
30
|
-
const sourceContext =
|
|
30
|
+
const sourceContext = protect.getSourceContext(fnName);
|
|
31
31
|
let securityException;
|
|
32
32
|
|
|
33
|
-
if (
|
|
34
|
-
|
|
35
|
-
} else {
|
|
36
|
-
if (req.body && Object.keys(req.body).length) {
|
|
37
|
-
sourceContext.parsedBody = req.body;
|
|
33
|
+
if (sourceContext && req.body && Object.keys(req.body).length) {
|
|
34
|
+
sourceContext.parsedBody = req.body;
|
|
38
35
|
|
|
36
|
+
if (fnName === 'bodyParser.text' && typeof req.body === 'string') {
|
|
39
37
|
try {
|
|
40
|
-
|
|
38
|
+
sourceContext.parsedBody = JSON.parse(req.body);
|
|
41
39
|
} catch (err) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
40
|
+
logger.error({ err }, 'Error parsing with bodyParser.text()');
|
|
41
|
+
origNext();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
inputAnalysis.handleParsedBody(sourceContext, sourceContext.parsedBody);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
if (isSecurityException(err)) {
|
|
50
|
+
securityException = err;
|
|
51
|
+
} else {
|
|
52
|
+
logger.error({ err }, 'Unexpected error during input analysis');
|
|
47
53
|
}
|
|
48
54
|
}
|
|
49
55
|
}
|
|
@@ -96,11 +102,7 @@ module.exports = (core) => {
|
|
|
96
102
|
function contrastHooked(...args) {
|
|
97
103
|
const parser = fn.original(...args);
|
|
98
104
|
const hookedParser = function (req, res, next) {
|
|
99
|
-
|
|
100
|
-
parser(req, res, next);
|
|
101
|
-
} else {
|
|
102
|
-
parser(req, res, contrastNext(req, next, fnName));
|
|
103
|
-
}
|
|
105
|
+
parser(req, res, contrastNext(req, next, fnName));
|
|
104
106
|
};
|
|
105
107
|
|
|
106
108
|
Object.defineProperty(hookedParser, 'name', {
|
|
@@ -23,7 +23,7 @@ module.exports = (core) => {
|
|
|
23
23
|
depHooks,
|
|
24
24
|
patcher,
|
|
25
25
|
logger,
|
|
26
|
-
|
|
26
|
+
protect,
|
|
27
27
|
protect: { inputAnalysis },
|
|
28
28
|
} = core;
|
|
29
29
|
|
|
@@ -40,23 +40,21 @@ module.exports = (core) => {
|
|
|
40
40
|
const [req, , origNext] = data.args;
|
|
41
41
|
|
|
42
42
|
function contrastNext(origErr) {
|
|
43
|
-
const sourceContext =
|
|
43
|
+
const sourceContext = protect.getSourceContext('cookie-parser');
|
|
44
|
+
|
|
44
45
|
let securityException;
|
|
45
46
|
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if ((req.cookies && Object.keys(req.cookies).length) || (req.signedCookies && Object.keys(req.signedCookies).length)) {
|
|
50
|
-
sourceContext.parsedCookies = { ...req.cookies, ...req.signedCookies };
|
|
47
|
+
if (sourceContext
|
|
48
|
+
&& ((req.cookies && Object.keys(req.cookies).length) || (req.signedCookies && Object.keys(req.signedCookies).length))) {
|
|
49
|
+
sourceContext.parsedCookies = { ...req.cookies, ...req.signedCookies };
|
|
51
50
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
51
|
+
try {
|
|
52
|
+
inputAnalysis.handleCookies(sourceContext, sourceContext.parsedCookies);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
if (isSecurityException(err)) {
|
|
55
|
+
securityException = err;
|
|
56
|
+
} else {
|
|
57
|
+
logger.error({ err }, 'Unexpected error during input analysis');
|
|
60
58
|
}
|
|
61
59
|
}
|
|
62
60
|
}
|
|
@@ -28,7 +28,7 @@ module.exports = (core) => {
|
|
|
28
28
|
depHooks,
|
|
29
29
|
patcher,
|
|
30
30
|
logger,
|
|
31
|
-
|
|
31
|
+
protect,
|
|
32
32
|
protect: { inputAnalysis },
|
|
33
33
|
} = core;
|
|
34
34
|
|
|
@@ -47,16 +47,13 @@ module.exports = (core) => {
|
|
|
47
47
|
const [req, , origNext] = data.args;
|
|
48
48
|
|
|
49
49
|
function contrastNext(origErr) {
|
|
50
|
-
const sourceContext =
|
|
50
|
+
const sourceContext = protect.getSourceContext('Express.query');
|
|
51
51
|
let securityException;
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
// which means that we've already handled/analyzed it.
|
|
58
|
-
// So we check whether we already have the `parsedQuery` property in the context
|
|
59
|
-
} else if (req.query && Object.keys(req.query).length && (!('parsedQuery' in sourceContext))) {
|
|
53
|
+
// It is possible for the query to be already parsed by `qs`
|
|
54
|
+
// which means that we've already handled/analyzed it.
|
|
55
|
+
// So we check whether we already have the `parsedQuery` property in the context
|
|
56
|
+
if (sourceContext && req.query && Object.keys(req.query).length && (!('parsedQuery' in sourceContext))) {
|
|
60
57
|
sourceContext.parsedQuery = req.query;
|
|
61
58
|
|
|
62
59
|
try {
|
|
@@ -87,11 +84,9 @@ module.exports = (core) => {
|
|
|
87
84
|
patchType,
|
|
88
85
|
pre(data) {
|
|
89
86
|
const { obj: { params } } = data;
|
|
90
|
-
const sourceContext =
|
|
87
|
+
const sourceContext = protect.getSourceContext('Express.Layer.handle_request');
|
|
91
88
|
|
|
92
|
-
if (
|
|
93
|
-
logger.debug('source context not available in `express.Layer.prototype.handle_request` hook');
|
|
94
|
-
} else if (params && Object.keys(params).length) {
|
|
89
|
+
if (sourceContext && params && Object.keys(params).length) {
|
|
95
90
|
sourceContext.parsedParams = params;
|
|
96
91
|
inputAnalysis.handleUrlParams(sourceContext, params);
|
|
97
92
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright: 2022 Contrast Security, Inc
|
|
3
|
+
* Contact: support@contrastsecurity.com
|
|
4
|
+
* License: Commercial
|
|
5
|
+
|
|
6
|
+
* NOTICE: This Software and the patented inventions embodied within may only be
|
|
7
|
+
* used as part of Contrast Security’s commercial offerings. Even though it is
|
|
8
|
+
* made available through public repositories, use of this Software is subject to
|
|
9
|
+
* the applicable End User Licensing Agreement found at
|
|
10
|
+
* https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
|
|
11
|
+
* between Contrast Security and the End User. The Software may not be reverse
|
|
12
|
+
* engineered, modified, repackaged, sold, redistributed or otherwise used in a
|
|
13
|
+
* way not consistent with the End User License Agreement.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const { patchType } = require('../constants');
|
|
19
|
+
const { isSecurityException } = require('../../security-exception');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Function that exports an install method to patch Fastify framework with our instrumentation
|
|
23
|
+
* @param {Object} core - the core Contrast object
|
|
24
|
+
* @return {Object} object with install method and the other relative functions exported for testing purposes
|
|
25
|
+
*/
|
|
26
|
+
module.exports = (core) => {
|
|
27
|
+
const {
|
|
28
|
+
depHooks,
|
|
29
|
+
patcher,
|
|
30
|
+
logger,
|
|
31
|
+
protect,
|
|
32
|
+
protect: { inputAnalysis },
|
|
33
|
+
} = core;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* registers a depHook for fastify module instrumentation
|
|
37
|
+
*/
|
|
38
|
+
function install() {
|
|
39
|
+
depHooks.resolve({ name: 'fastify', version: '>=3 <5' }, (fastify) => {
|
|
40
|
+
return patcher.patch(fastify, {
|
|
41
|
+
name: 'fastify.build',
|
|
42
|
+
patchType,
|
|
43
|
+
post({ result: server }) {
|
|
44
|
+
server.addHook('preValidation', function(request, reply, done) {
|
|
45
|
+
let securityException;
|
|
46
|
+
const sourceContext = protect.getSourceContext('Fastify.preValidationHook');
|
|
47
|
+
|
|
48
|
+
if (sourceContext) {
|
|
49
|
+
try {
|
|
50
|
+
if (request.params) {
|
|
51
|
+
sourceContext.parsedParams = request.params;
|
|
52
|
+
inputAnalysis.handleUrlParams(sourceContext, request.params);
|
|
53
|
+
}
|
|
54
|
+
if (request.cookies) {
|
|
55
|
+
sourceContext.parsedCookies = request.cookies;
|
|
56
|
+
inputAnalysis.handleCookies(sourceContext, request.cookies);
|
|
57
|
+
}
|
|
58
|
+
if (request.body) {
|
|
59
|
+
sourceContext.parsedBody = request.body;
|
|
60
|
+
inputAnalysis.handleParsedBody(sourceContext, request.body);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (request.query) {
|
|
64
|
+
sourceContext.parsedQuery = request.query;
|
|
65
|
+
inputAnalysis.handleQueryParams(sourceContext, request.query);
|
|
66
|
+
}
|
|
67
|
+
} catch (err) {
|
|
68
|
+
if (isSecurityException(err)) {
|
|
69
|
+
securityException = err;
|
|
70
|
+
} else {
|
|
71
|
+
logger.error({ err }, 'Unexpected error during input analysis');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
done(securityException);
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return inputAnalysis.fastifyInstrumentation = {
|
|
84
|
+
install,
|
|
85
|
+
};
|
|
86
|
+
};
|