@contrast/protect 1.2.1 → 1.4.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/constants.js +15 -0
- package/lib/error-handlers/index.js +17 -0
- package/lib/error-handlers/install/express4.js +89 -0
- package/lib/error-handlers/install/fastify3.js +17 -4
- package/lib/error-handlers/install/koa2.js +16 -2
- package/lib/esm-loader.mjs +15 -0
- package/lib/get-source-context.js +33 -0
- 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 +127 -19
- package/lib/index.js +19 -0
- package/lib/input-analysis/constants.js +20 -0
- package/lib/input-analysis/handlers.js +201 -16
- package/lib/input-analysis/index.js +40 -3
- package/lib/input-analysis/install/body-parser1.js +122 -0
- package/lib/input-analysis/install/cookie-parser1.js +80 -0
- package/lib/input-analysis/install/express4.js +103 -0
- package/lib/input-analysis/install/fastify3.js +51 -24
- package/lib/input-analysis/install/formidable1.js +72 -0
- package/lib/input-analysis/install/http.js +30 -4
- package/lib/input-analysis/install/koa-body5.js +63 -0
- package/lib/input-analysis/install/koa-bodyparser4.js +64 -0
- package/lib/input-analysis/install/koa2.js +38 -48
- package/lib/input-analysis/install/multer1.js +88 -0
- package/lib/input-analysis/install/qs6.js +57 -0
- package/lib/input-analysis/install/universal-cookie4.js +52 -0
- package/lib/input-analysis/ip-analysis.js +76 -0
- package/lib/input-analysis/virtual-patches.js +109 -0
- package/lib/input-tracing/constants.js +15 -0
- package/lib/input-tracing/handlers/index.js +225 -66
- package/lib/input-tracing/index.js +25 -2
- package/lib/input-tracing/install/child-process.js +28 -7
- package/lib/input-tracing/install/eval.js +60 -0
- package/lib/input-tracing/install/fs.js +21 -4
- package/lib/input-tracing/install/http.js +63 -0
- package/lib/input-tracing/install/mongodb.js +233 -0
- package/lib/input-tracing/install/mysql.js +21 -4
- package/lib/input-tracing/install/postgres.js +20 -4
- package/lib/input-tracing/install/sequelize.js +22 -5
- package/lib/input-tracing/install/sqlite3.js +21 -4
- package/lib/input-tracing/install/vm.js +132 -0
- package/lib/make-response-blocker.js +15 -0
- package/lib/make-source-context.js +22 -1
- package/lib/security-exception.js +15 -0
- package/lib/semantic-analysis/handlers.js +160 -0
- package/lib/semantic-analysis/index.js +38 -0
- package/lib/throw-security-exception.js +17 -6
- package/package.json +10 -12
- package/lib/cli-rewriter.js +0 -20
- package/lib/input-analysis/install/co-body.js +0 -51
- package/lib/input-analysis/install/cookie-parser.js +0 -48
- package/lib/input-analysis/install/formidable.js +0 -53
- package/lib/input-analysis/install/multer.js +0 -52
- package/lib/input-analysis/install/qs.js +0 -40
- package/lib/input-analysis/install/universal-cookie.js +0 -34
- package/lib/input-tracing/handlers/nosql-injection-mongo.js +0 -48
- package/lib/utils.js +0 -88
|
@@ -1,6 +1,22 @@
|
|
|
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
|
+
|
|
1
16
|
'use strict';
|
|
2
17
|
|
|
3
|
-
const { simpleTraverse } = require('
|
|
18
|
+
const { BLOCKING_MODES, simpleTraverse } = require('@contrast/common');
|
|
19
|
+
const address = require('ipaddr.js');
|
|
4
20
|
|
|
5
21
|
//
|
|
6
22
|
// these rules are not implemented by agent-lib, but are being considered for
|
|
@@ -95,8 +111,12 @@ module.exports = function(core) {
|
|
|
95
111
|
* @returns {undefined|[String]} undefined to permit else [mode, rule] to block.
|
|
96
112
|
*/
|
|
97
113
|
inputAnalysis.handleConnect = function handleConnect(sourceContext, connectInputs) {
|
|
114
|
+
if (!sourceContext || sourceContext.allowed) return;
|
|
115
|
+
|
|
98
116
|
const { rules: { agentLibRules, agentLibRulesMask: mask } } = sourceContext;
|
|
99
117
|
|
|
118
|
+
inputAnalysis.handleVirtualPatches(sourceContext, { URLS: connectInputs.rawUrl, HEADERS: connectInputs.headers });
|
|
119
|
+
|
|
100
120
|
// initialize findings to the basics
|
|
101
121
|
let block = undefined;
|
|
102
122
|
if (mask !== 0) {
|
|
@@ -115,12 +135,12 @@ module.exports = function(core) {
|
|
|
115
135
|
throw new Error('nyi', sourceContext);
|
|
116
136
|
};
|
|
117
137
|
|
|
118
|
-
|
|
119
138
|
const jsonInputTypes = {
|
|
120
|
-
keyType: agentLib.InputType.JsonKey,
|
|
139
|
+
keyType: agentLib.InputType.JsonKey, inputType: agentLib.InputType.JsonValue
|
|
121
140
|
};
|
|
141
|
+
|
|
122
142
|
const parameterInputTypes = {
|
|
123
|
-
keyType: agentLib.InputType.ParameterKey,
|
|
143
|
+
keyType: agentLib.InputType.ParameterKey, inputType: agentLib.InputType.ParameterValue
|
|
124
144
|
};
|
|
125
145
|
|
|
126
146
|
/**
|
|
@@ -140,11 +160,22 @@ module.exports = function(core) {
|
|
|
140
160
|
* @param {Object} queryParams pojo {key: value, ...} for all query params/search params
|
|
141
161
|
*/
|
|
142
162
|
inputAnalysis.handleQueryParams = function handleQueryParams(sourceContext, queryParams) {
|
|
163
|
+
if (sourceContext.analyzedQuery) return;
|
|
164
|
+
sourceContext.analyzedQuery = true;
|
|
165
|
+
|
|
143
166
|
if (typeof queryParams !== 'object') {
|
|
144
167
|
logger.debug({ queryParams }, 'handleQueryParams() called with non-object');
|
|
145
168
|
return;
|
|
146
169
|
}
|
|
147
|
-
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
inputAnalysis.handleVirtualPatches(sourceContext, { PARAMETERS: queryParams });
|
|
173
|
+
|
|
174
|
+
const block = commonObjectAnalyzer(sourceContext, queryParams, parameterInputTypes);
|
|
175
|
+
|
|
176
|
+
if (block) {
|
|
177
|
+
core.protect.throwSecurityException(sourceContext);
|
|
178
|
+
}
|
|
148
179
|
};
|
|
149
180
|
|
|
150
181
|
/**
|
|
@@ -157,11 +188,16 @@ module.exports = function(core) {
|
|
|
157
188
|
* @param {Object} urlParams pojo
|
|
158
189
|
*/
|
|
159
190
|
inputAnalysis.handleUrlParams = function(sourceContext, urlParams) {
|
|
191
|
+
if (sourceContext.analyzedUrlParams) return;
|
|
192
|
+
sourceContext.analyzedUrlParams = true;
|
|
193
|
+
|
|
160
194
|
if (typeof urlParams !== 'object') {
|
|
161
195
|
logger.debug({ urlParams }, 'handleUrlParams() called with non-object');
|
|
162
196
|
return;
|
|
163
197
|
}
|
|
164
198
|
|
|
199
|
+
inputAnalysis.handleVirtualPatches(sourceContext, { PARAMETERS: urlParams });
|
|
200
|
+
|
|
165
201
|
const { rules, rules: { agentLibRulesMask: mask } } = sourceContext;
|
|
166
202
|
const resultsList = [];
|
|
167
203
|
const { UrlParameter } = agentLib.InputType;
|
|
@@ -199,10 +235,11 @@ module.exports = function(core) {
|
|
|
199
235
|
securityException: undefined,
|
|
200
236
|
resultsList,
|
|
201
237
|
};
|
|
238
|
+
|
|
202
239
|
const block = mergeFindings(rules.agentLibRules, sourceContext.findings, urlParamsFindings);
|
|
203
240
|
|
|
204
241
|
if (block) {
|
|
205
|
-
|
|
242
|
+
core.protect.throwSecurityException(sourceContext);
|
|
206
243
|
}
|
|
207
244
|
};
|
|
208
245
|
|
|
@@ -215,17 +252,23 @@ module.exports = function(core) {
|
|
|
215
252
|
* @param {Object} cookies pojo
|
|
216
253
|
*/
|
|
217
254
|
inputAnalysis.handleCookies = function(sourceContext, cookies) {
|
|
255
|
+
if (sourceContext.analyzedCookies) return;
|
|
256
|
+
sourceContext.analyzedCookies = true;
|
|
257
|
+
|
|
218
258
|
const cookiesArr = Object.entries(cookies).reduce((acc, [key, value]) => {
|
|
219
259
|
acc.push(key, value);
|
|
220
260
|
return acc;
|
|
221
261
|
}, []);
|
|
262
|
+
|
|
263
|
+
inputAnalysis.handleVirtualPatches(sourceContext, { HEADERS: cookies });
|
|
264
|
+
|
|
222
265
|
const { rules, rules: { agentLibRulesMask: mask } } = sourceContext;
|
|
223
266
|
const cookieFindings = agentLib.scoreRequestConnect(mask, { cookies: cookiesArr }, preferWW);
|
|
224
267
|
|
|
225
268
|
const block = mergeFindings(rules.agentLibRules, sourceContext.findings, cookieFindings);
|
|
226
269
|
|
|
227
270
|
if (block) {
|
|
228
|
-
|
|
271
|
+
core.protect.throwSecurityException(sourceContext);
|
|
229
272
|
}
|
|
230
273
|
};
|
|
231
274
|
|
|
@@ -239,11 +282,16 @@ module.exports = function(core) {
|
|
|
239
282
|
* @param {Object} parsedBody
|
|
240
283
|
*/
|
|
241
284
|
inputAnalysis.handleParsedBody = function(sourceContext, parsedBody) {
|
|
285
|
+
if (sourceContext.analyzedBody) return;
|
|
286
|
+
sourceContext.analyzedBody = true;
|
|
287
|
+
|
|
242
288
|
if (typeof parsedBody !== 'object') {
|
|
243
289
|
logger.debug({ parsedBody }, 'handleParsedBody() called with non-object');
|
|
244
290
|
return;
|
|
245
291
|
}
|
|
246
292
|
|
|
293
|
+
inputAnalysis.handleVirtualPatches(sourceContext, { PARAMETERS: parsedBody });
|
|
294
|
+
|
|
247
295
|
let bodyType;
|
|
248
296
|
let inputTypes;
|
|
249
297
|
if (sourceContext.reqData.contentType.includes('/json')) {
|
|
@@ -253,8 +301,13 @@ module.exports = function(core) {
|
|
|
253
301
|
bodyType = 'urlencoded';
|
|
254
302
|
inputTypes = parameterInputTypes;
|
|
255
303
|
}
|
|
256
|
-
commonObjectAnalyzer(sourceContext, parsedBody, inputTypes);
|
|
304
|
+
const block = commonObjectAnalyzer(sourceContext, parsedBody, inputTypes);
|
|
305
|
+
|
|
257
306
|
sourceContext.findings.bodyType = bodyType;
|
|
307
|
+
|
|
308
|
+
if (block) {
|
|
309
|
+
core.protect.throwSecurityException(sourceContext);
|
|
310
|
+
}
|
|
258
311
|
};
|
|
259
312
|
|
|
260
313
|
// was MULTIPART_NAME but maybe we should just call it what it is. it's kind
|
|
@@ -263,6 +316,70 @@ module.exports = function(core) {
|
|
|
263
316
|
throw new Error('nyi', sourceContext, name);
|
|
264
317
|
};
|
|
265
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
|
+
};
|
|
266
383
|
|
|
267
384
|
/**
|
|
268
385
|
* commonObjectAnalyzer() walks an object supplied by the end-user and checks
|
|
@@ -277,12 +394,12 @@ module.exports = function(core) {
|
|
|
277
394
|
* @param {Object} inputTypes is either jsonInputTypes or parameterInputTypes,
|
|
278
395
|
* both are defined above. They specify the input types to be used when evaluating
|
|
279
396
|
* the object.
|
|
280
|
-
* @returns undefined
|
|
397
|
+
* @returns {Array | undefined} returns an array with block info if vulnerability was found.
|
|
281
398
|
*/
|
|
282
399
|
function commonObjectAnalyzer(sourceContext, object, inputTypes) {
|
|
283
400
|
const { rules, rules: { agentLibRulesMask: mask } } = sourceContext;
|
|
284
401
|
// use inputTypes to set params...
|
|
285
|
-
const { keyType,
|
|
402
|
+
const { keyType, inputType } = inputTypes;
|
|
286
403
|
const inputTypeStr = inputTypes === jsonInputTypes ? 'Json' : 'Parameter';
|
|
287
404
|
const { Where } = agentLib.MongoQueryType;
|
|
288
405
|
const resultsList = [];
|
|
@@ -315,7 +432,7 @@ module.exports = function(core) {
|
|
|
315
432
|
mongoQueryType = agentLib.getMongoQueryType(value);
|
|
316
433
|
}
|
|
317
434
|
} else {
|
|
318
|
-
itemType =
|
|
435
|
+
itemType = inputType;
|
|
319
436
|
}
|
|
320
437
|
let items = agentLib.scoreAtom(mask, value, itemType, preferWW);
|
|
321
438
|
if (!items && !mongoQueryType) {
|
|
@@ -333,7 +450,8 @@ module.exports = function(core) {
|
|
|
333
450
|
// mimic it here (where scoreAtom() was used). the actual object/string
|
|
334
451
|
// to match is stored as `inputToCheck`.
|
|
335
452
|
const inputType = typeof inputToCheck;
|
|
336
|
-
|
|
453
|
+
// query types up to Where, inclusive, accept either string or object values. Where and above accept only string values
|
|
454
|
+
if (mongoQueryType <= Where || inputType === 'string') {
|
|
337
455
|
// the query-type/input-type combination is valid. add a synthesized item.
|
|
338
456
|
const item = { ruleId: 'nosql-injection-mongo', score: 10, mongoContext: { inputToCheck } };
|
|
339
457
|
items.push(item);
|
|
@@ -370,9 +488,50 @@ module.exports = function(core) {
|
|
|
370
488
|
resultsList,
|
|
371
489
|
};
|
|
372
490
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
491
|
+
return mergeFindings(rules.agentLibRules, sourceContext.findings, findings);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function ipListAnalysis(reqIp, reqHeaders, list) {
|
|
495
|
+
const forwardedIps = [];
|
|
496
|
+
|
|
497
|
+
for (let i = 0; i < reqHeaders.length; i++) {
|
|
498
|
+
if (reqHeaders[i] === 'x-forwarded-for') {
|
|
499
|
+
const ipsFromHeaders = reqHeaders[i + 1]?.split(/[,;]+/);
|
|
500
|
+
forwardedIps.push(...ipsFromHeaders);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const ipsToCheck = [reqIp, ...forwardedIps];
|
|
505
|
+
const now = new Date().getTime();
|
|
506
|
+
|
|
507
|
+
/* c8 ignore next 3 */
|
|
508
|
+
if (!ipsToCheck.length) {
|
|
509
|
+
return false;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
for (const listEntry of list) {
|
|
513
|
+
const { doesExpire, expiresAt } = listEntry;
|
|
514
|
+
for (let i = 0; i < ipsToCheck.length; i++) {
|
|
515
|
+
const currentIp = ipsToCheck[i];
|
|
516
|
+
|
|
517
|
+
// Ignore bad IP values.
|
|
518
|
+
if (!address.isValid(currentIp)) {
|
|
519
|
+
logger.warn(`Unable to parse ${currentIp}.`);
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
const expired = doesExpire ? expiresAt - now <= 0 : false;
|
|
523
|
+
|
|
524
|
+
if (expired) {
|
|
525
|
+
logger.info(`IP expired: ${listEntry.name}, ${listEntry.ip}`);
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const match = checkIpsMatch(listEntry, currentIp);
|
|
530
|
+
|
|
531
|
+
if (match) {
|
|
532
|
+
return match;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
376
535
|
}
|
|
377
536
|
}
|
|
378
537
|
};
|
|
@@ -432,13 +591,38 @@ function normalizeFindings(rules, findings) {
|
|
|
432
591
|
// which the block can occur. so at a minimum 'block' should also result in a
|
|
433
592
|
// block.
|
|
434
593
|
const { mode } = rules[r.ruleId];
|
|
435
|
-
if (r.score >= 90 &&
|
|
594
|
+
if (r.score >= 90 && BLOCKING_MODES.includes(mode)) {
|
|
436
595
|
r.blocked = true;
|
|
437
596
|
findings.securityException = [mode, r.ruleId];
|
|
438
597
|
}
|
|
439
598
|
}
|
|
440
599
|
}
|
|
441
600
|
|
|
601
|
+
|
|
602
|
+
function checkIpsMatch(listEntry, ip) {
|
|
603
|
+
const parsed = address.process(ip);
|
|
604
|
+
|
|
605
|
+
// Check if IP is in CIDR range,
|
|
606
|
+
if (listEntry.cidr) {
|
|
607
|
+
if (parsed.kind() !== listEntry.cidr.kind) {
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (parsed.match(listEntry.cidr.range)) {
|
|
612
|
+
return { ...listEntry, match: ip };
|
|
613
|
+
} else {
|
|
614
|
+
return null;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// or do a direct comparison
|
|
619
|
+
if (parsed.toNormalizedString() === listEntry.normalizedValue) {
|
|
620
|
+
return { ...listEntry, matchedIp: ip };
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
return null;
|
|
624
|
+
}
|
|
625
|
+
|
|
442
626
|
/**
|
|
443
627
|
* getValueAtKey() is used to fetch the object (expected) associated
|
|
444
628
|
* with the path of keys in obj. i say expected because this is only used
|
|
@@ -453,6 +637,7 @@ function normalizeFindings(rules, findings) {
|
|
|
453
637
|
*/
|
|
454
638
|
function getValueAtKey(obj, path, key) {
|
|
455
639
|
for (const p of path) {
|
|
640
|
+
/* c8 ignore next 6 */
|
|
456
641
|
if (!(p in obj)) {
|
|
457
642
|
return undefined;
|
|
458
643
|
}
|
|
@@ -1,17 +1,54 @@
|
|
|
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
|
+
|
|
1
16
|
'use strict';
|
|
2
17
|
|
|
3
18
|
module.exports = function(core) {
|
|
4
19
|
const inputAnalysis = core.protect.inputAnalysis = {};
|
|
5
20
|
|
|
21
|
+
// inputAnalysis handlers
|
|
6
22
|
require('./handlers')(core);
|
|
23
|
+
|
|
24
|
+
// http(s) modules instrumentation
|
|
7
25
|
require('./install/http')(core);
|
|
26
|
+
|
|
27
|
+
// common libraries instrumentation
|
|
28
|
+
require('./install/body-parser1')(core);
|
|
29
|
+
require('./install/cookie-parser1')(core);
|
|
30
|
+
require('./install/formidable1')(core);
|
|
31
|
+
require('./install/koa-body5')(core);
|
|
32
|
+
require('./install/koa-bodyparser4')(core);
|
|
33
|
+
require('./install/multer1')(core);
|
|
34
|
+
require('./install/qs6')(core);
|
|
35
|
+
require('./install/universal-cookie4')(core);
|
|
36
|
+
|
|
37
|
+
// framework specific instrumentation
|
|
8
38
|
require('./install/fastify3')(core);
|
|
9
39
|
require('./install/koa2')(core);
|
|
40
|
+
require('./install/express4')(core);
|
|
41
|
+
|
|
42
|
+
// virtual patches
|
|
43
|
+
require('./virtual-patches')(core);
|
|
44
|
+
require('./ip-analysis')(core);
|
|
10
45
|
|
|
11
46
|
inputAnalysis.install = function() {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
47
|
+
Object.values(inputAnalysis)
|
|
48
|
+
.filter((property) => property.install)
|
|
49
|
+
.forEach((library) => {
|
|
50
|
+
library.install();
|
|
51
|
+
});
|
|
15
52
|
};
|
|
16
53
|
|
|
17
54
|
return inputAnalysis;
|
|
@@ -0,0 +1,122 @@
|
|
|
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 { isSecurityException } = require('../../security-exception');
|
|
19
|
+
|
|
20
|
+
module.exports = (core) => {
|
|
21
|
+
const {
|
|
22
|
+
depHooks,
|
|
23
|
+
logger,
|
|
24
|
+
protect,
|
|
25
|
+
protect: { inputAnalysis },
|
|
26
|
+
} = core;
|
|
27
|
+
|
|
28
|
+
function contrastNext(req, origNext, fnName) {
|
|
29
|
+
return function next(origErr) {
|
|
30
|
+
const sourceContext = protect.getSourceContext(fnName);
|
|
31
|
+
let securityException;
|
|
32
|
+
|
|
33
|
+
if (sourceContext && req.body && Object.keys(req.body).length) {
|
|
34
|
+
sourceContext.parsedBody = req.body;
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
inputAnalysis.handleParsedBody(sourceContext, req.body);
|
|
38
|
+
} catch (err) {
|
|
39
|
+
if (isSecurityException(err)) {
|
|
40
|
+
securityException = err;
|
|
41
|
+
} else {
|
|
42
|
+
logger.error({ err }, 'Unexpected error during input analysis');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const error = securityException || origErr;
|
|
47
|
+
|
|
48
|
+
origNext(error);
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Patch body parser - `body-parser` used by `express` framework
|
|
53
|
+
function install() {
|
|
54
|
+
depHooks.resolve({ name: 'body-parser' }, (bodyParser) => {
|
|
55
|
+
const origBodyParser = bodyParser;
|
|
56
|
+
|
|
57
|
+
const { json: origJson, raw: origRaw, text: origText, urlencoded: origUrlencoded } = bodyParser;
|
|
58
|
+
const fnArr = [
|
|
59
|
+
{
|
|
60
|
+
key: 'json',
|
|
61
|
+
original: origJson,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
key: 'raw',
|
|
65
|
+
original: origRaw,
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
key: 'text',
|
|
69
|
+
original: origText,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
key: 'urlencoded',
|
|
73
|
+
original: origUrlencoded,
|
|
74
|
+
}
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
bodyParser = function bodyParser(...args) {
|
|
78
|
+
const parser = origBodyParser(...args);
|
|
79
|
+
const hookedParser = function(req, res, next) {
|
|
80
|
+
parser(req, res, contrastNext(req, next, 'bodyParser'));
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
Object.defineProperty(hookedParser, 'name', {
|
|
84
|
+
value: 'bodyParser'
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return hookedParser;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
fnArr.forEach((fn) => {
|
|
91
|
+
const fnName = `bodyParser.${fn.key}`;
|
|
92
|
+
function contrastHooked(...args) {
|
|
93
|
+
const parser = fn.original(...args);
|
|
94
|
+
const hookedParser = function (req, res, next) {
|
|
95
|
+
parser(req, res, contrastNext(req, next, fnName));
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
Object.defineProperty(hookedParser, 'name', {
|
|
99
|
+
value: `${fn.key}Parser`
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return hookedParser;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
Object.defineProperty(bodyParser, fn.key, {
|
|
106
|
+
configurable: true,
|
|
107
|
+
enumerable: true,
|
|
108
|
+
get: () => contrastHooked,
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return bodyParser;
|
|
113
|
+
}
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const bodyParser1Instrumentation = inputAnalysis.bodyParser1Instrumentation = {
|
|
118
|
+
install
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
return bodyParser1Instrumentation;
|
|
122
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
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
|
+
module.exports = (core) => {
|
|
22
|
+
const {
|
|
23
|
+
depHooks,
|
|
24
|
+
patcher,
|
|
25
|
+
logger,
|
|
26
|
+
protect,
|
|
27
|
+
protect: { inputAnalysis },
|
|
28
|
+
} = core;
|
|
29
|
+
|
|
30
|
+
// Patch `cookie-parser` package
|
|
31
|
+
function install() {
|
|
32
|
+
depHooks.resolve({ name: 'cookie-parser' }, (cookieParser) => patcher.patch(cookieParser, {
|
|
33
|
+
name: 'cookie-parser',
|
|
34
|
+
patchType,
|
|
35
|
+
post(data) {
|
|
36
|
+
data.result = patcher.patch(data.result, {
|
|
37
|
+
name: 'cookie-parser',
|
|
38
|
+
patchType,
|
|
39
|
+
pre(data) {
|
|
40
|
+
const [req, , origNext] = data.args;
|
|
41
|
+
|
|
42
|
+
function contrastNext(origErr) {
|
|
43
|
+
const sourceContext = protect.getSourceContext('cookie-parser');
|
|
44
|
+
|
|
45
|
+
let securityException;
|
|
46
|
+
|
|
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 };
|
|
50
|
+
|
|
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');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const error = securityException || origErr;
|
|
63
|
+
|
|
64
|
+
origNext(error);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
data.args[2] = contrastNext;
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const cookieParser1Instrumentation = inputAnalysis.cookieParser1Instrumentation = {
|
|
76
|
+
install
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return cookieParser1Instrumentation;
|
|
80
|
+
};
|