@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.
Files changed (49) hide show
  1. package/lib/error-handlers/index.js +7 -4
  2. package/lib/error-handlers/install/express4.js +2 -3
  3. package/lib/error-handlers/install/{fastify3.js → fastify.js} +13 -15
  4. package/lib/error-handlers/install/hapi.js +75 -0
  5. package/lib/error-handlers/install/koa2.js +1 -2
  6. package/lib/{cli-rewriter.js → get-source-context.js} +13 -15
  7. package/lib/hardening/constants.js +20 -0
  8. package/lib/hardening/handlers.js +65 -0
  9. package/lib/hardening/index.js +29 -0
  10. package/lib/hardening/install/node-serialize0.js +59 -0
  11. package/lib/index.d.ts +3 -21
  12. package/lib/index.js +6 -46
  13. package/lib/input-analysis/handlers.js +198 -39
  14. package/lib/input-analysis/index.js +9 -6
  15. package/lib/input-analysis/install/body-parser1.js +20 -18
  16. package/lib/input-analysis/install/cookie-parser1.js +13 -15
  17. package/lib/input-analysis/install/express4.js +8 -13
  18. package/lib/input-analysis/install/fastify.js +86 -0
  19. package/lib/input-analysis/install/formidable1.js +4 -5
  20. package/lib/input-analysis/install/hapi.js +106 -0
  21. package/lib/input-analysis/install/http.js +80 -30
  22. package/lib/input-analysis/install/koa-body5.js +5 -10
  23. package/lib/input-analysis/install/koa-bodyparser4.js +6 -10
  24. package/lib/input-analysis/install/koa2.js +13 -24
  25. package/lib/input-analysis/install/multer1.js +5 -6
  26. package/lib/input-analysis/install/qs6.js +7 -11
  27. package/lib/input-analysis/install/universal-cookie4.js +3 -7
  28. package/lib/input-analysis/ip-analysis.js +76 -0
  29. package/lib/input-analysis/virtual-patches.js +109 -0
  30. package/lib/input-tracing/handlers/index.js +92 -23
  31. package/lib/input-tracing/index.js +14 -18
  32. package/lib/input-tracing/install/child-process.js +13 -7
  33. package/lib/input-tracing/install/eval.js +60 -0
  34. package/lib/input-tracing/install/fs.js +4 -2
  35. package/lib/input-tracing/install/function.js +60 -0
  36. package/lib/input-tracing/install/http.js +63 -0
  37. package/lib/input-tracing/install/mongodb.js +20 -20
  38. package/lib/input-tracing/install/mysql.js +3 -2
  39. package/lib/input-tracing/install/postgres.js +5 -4
  40. package/lib/input-tracing/install/sequelize.js +7 -5
  41. package/lib/input-tracing/install/sqlite3.js +6 -4
  42. package/lib/input-tracing/install/vm.js +132 -0
  43. package/lib/make-source-context.js +8 -49
  44. package/lib/policy.js +134 -0
  45. package/lib/semantic-analysis/handlers.js +161 -0
  46. package/lib/semantic-analysis/index.js +38 -0
  47. package/package.json +7 -9
  48. package/lib/input-analysis/install/fastify3.js +0 -107
  49. package/lib/utils.js +0 -84
@@ -15,7 +15,8 @@
15
15
 
16
16
  'use strict';
17
17
 
18
- const { simpleTraverse } = require('../utils');
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
- const { rules: { agentLibRules, agentLibRulesMask: mask } } = sourceContext;
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 (mask !== 0) {
118
- const findings = agentLib.scoreRequestConnect(mask, connectInputs, preferWW);
119
- block = mergeFindings(agentLibRules, sourceContext.findings, findings);
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, valueType: agentLib.InputType.JsonValue
139
+ keyType: agentLib.InputType.JsonKey, inputType: agentLib.InputType.JsonValue
136
140
  };
141
+
137
142
  const parameterInputTypes = {
138
- keyType: agentLib.InputType.ParameterKey, valueType: agentLib.InputType.ParameterValue
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
- const { rules, rules: { agentLibRulesMask: mask } } = sourceContext;
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(mask, value, UrlParameter, preferWW);
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(rules.agentLibRules, sourceContext.findings, urlParamsFindings);
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
- const block = mergeFindings(rules.agentLibRules, sourceContext.findings, cookieFindings);
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 { rules, rules: { agentLibRulesMask: mask } } = sourceContext;
400
+ const { policy: { rulesMask } } = sourceContext;
401
+ if (!rulesMask) return;
402
+
309
403
  // use inputTypes to set params...
310
- const { keyType, valueType } = inputTypes;
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 mongoQueryType;
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 (mask & agentLib.RuleType['nosql-injection-mongo']) {
340
- mongoQueryType = agentLib.getMongoQueryType(value);
432
+ if (rulesMask & agentLib.RuleType['nosql-injection-mongo']) {
433
+ isMongoQueryType = agentLib.isMongoQueryType(value);
341
434
  }
342
435
  } else {
343
- itemType = valueType;
436
+ itemType = inputType;
344
437
  }
345
- let items = agentLib.scoreAtom(mask, value, itemType, preferWW);
346
- if (!items && !mongoQueryType) {
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 (mongoQueryType) {
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 inputType = typeof inputToCheck;
361
- // query types up to Where, inclusive, accept either string or object values. Where and above accept only string values
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
- // mimic scoreRequestConnect() returning the query type as the value
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(rules.agentLibRules, sourceContext.findings, findings);
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(rules, findings, newFindings) {
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(rules, newFindings);
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(rules, findings) {
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 { mode } = rules[r.ruleId];
458
- if (r.score >= 90 && ['block', 'block_at_perimeter'].includes(mode)) {
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/fastify3')(core);
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
- Object.values(inputAnalysis)
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
- scopes: { sources },
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 = sources.getStore()?.protect;
30
+ const sourceContext = protect.getSourceContext(fnName);
31
31
  let securityException;
32
32
 
33
- if (!sourceContext) {
34
- logger.debug(`source context not available in \`body-parser\`'s \`${fnName}\` hook`);
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
- inputAnalysis.handleParsedBody(sourceContext, req.body);
38
+ sourceContext.parsedBody = JSON.parse(req.body);
41
39
  } catch (err) {
42
- if (isSecurityException(err)) {
43
- securityException = err;
44
- } else {
45
- logger.error({ err }, 'Unexpected error during input analysis');
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
- if (!req.body) {
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
- scopes: { sources },
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 = sources.getStore()?.protect;
43
+ const sourceContext = protect.getSourceContext('cookie-parser');
44
+
44
45
  let securityException;
45
46
 
46
- if (!sourceContext) {
47
- logger.debug('source context not available in `cookie-parser` hook');
48
- } else {
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
- try {
53
- inputAnalysis.handleCookies(sourceContext, sourceContext.parsedCookies);
54
- } catch (err) {
55
- if (isSecurityException(err)) {
56
- securityException = err;
57
- } else {
58
- logger.error({ err }, 'Unexpected error during input analysis');
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
- scopes: { sources },
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 = sources.getStore()?.protect;
50
+ const sourceContext = protect.getSourceContext('Express.query');
51
51
  let securityException;
52
52
 
53
- if (!sourceContext) {
54
- logger.debug('source context not available in `Express.query` hook');
55
-
56
- // It is possible for the query to be already parsed by `qs`
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 = sources.getStore()?.protect;
87
+ const sourceContext = protect.getSourceContext('Express.Layer.handle_request');
91
88
 
92
- if (!sourceContext) {
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
+ };