@contrast/protect 1.4.0 → 1.6.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.
@@ -18,14 +18,17 @@
18
18
  module.exports = function(core) {
19
19
  const errorHandlers = core.protect.errorHandlers = {};
20
20
 
21
- require('./install/fastify3')(core);
21
+ require('./install/fastify')(core);
22
22
  require('./install/koa2')(core);
23
23
  require('./install/express4')(core);
24
+ require('./install/hapi')(core);
24
25
 
25
26
  errorHandlers.install = function() {
26
- errorHandlers.fastify3ErrorHandler.install();
27
- errorHandlers.koa2ErrorHandler.install();
28
- errorHandlers.express4ErrorHandler.install();
27
+ for (const component of Object.values(errorHandlers)) {
28
+ if (component.install) {
29
+ component.install();
30
+ }
31
+ }
29
32
  };
30
33
 
31
34
  return errorHandlers;
@@ -25,7 +25,7 @@ module.exports = function(core) {
25
25
  protect,
26
26
  } = core;
27
27
 
28
- const fastify3ErrorHandler = protect.errorHandlers.fastify3ErrorHandler = {
28
+ const fastifyErrorHandler = protect.errorHandlers.fastifyErrorHandler = {
29
29
  _userHandler: null
30
30
  };
31
31
 
@@ -33,7 +33,7 @@ module.exports = function(core) {
33
33
  * This is the default handler from fastify's source code. If it's not a
34
34
  * Contrast error and the user didn't supply their own we should use this.
35
35
  */
36
- fastify3ErrorHandler.defaultErrorHandler = function (error, request, reply) {
36
+ fastifyErrorHandler.defaultErrorHandler = function (error, request, reply) {
37
37
  if (reply.statusCode < 500) {
38
38
  reply.log.info({ res: reply, err: error }, error && error.message);
39
39
  } else {
@@ -47,17 +47,17 @@ module.exports = function(core) {
47
47
 
48
48
  /**
49
49
  * Check if the error being handled was thrown by Contrast. If not,
50
- * use either the default fastify3 error handler or the user-defined handler
50
+ * use either the default fastify error handler or the user-defined handler
51
51
  * if one was specified by calling fastify.setErrorHandler(fn).
52
52
  * @param {error} err error being handled
53
53
  * @param {object} request fastify request
54
54
  * @param {object} reply fastify repoly
55
55
  */
56
- fastify3ErrorHandler.handler = function(err, request, reply) {
57
- const normalHandler = fastify3ErrorHandler._userHandler || fastify3ErrorHandler.defaultErrorHandler;
56
+ fastifyErrorHandler.handler = function(err, request, reply) {
57
+ const normalHandler = fastifyErrorHandler._userHandler || fastifyErrorHandler.defaultErrorHandler;
58
58
 
59
59
  if (isSecurityException(err)) {
60
- const sourceContext = protect.getSourceContext('fastify3.errorHandler');
60
+ const sourceContext = protect.getSourceContext('fastify.errorHandler');
61
61
 
62
62
  if (!sourceContext) {
63
63
  normalHandler.call(this, err, request, reply);
@@ -73,14 +73,14 @@ module.exports = function(core) {
73
73
  /**
74
74
  * Instruments fastify in order to add our custom error handler.
75
75
  */
76
- fastify3ErrorHandler.install = function() {
77
- depHooks.resolve({ name: 'fastify', version: '<=4.0.0' }, (fastify) => patcher.patch(fastify, {
76
+ fastifyErrorHandler.install = function() {
77
+ depHooks.resolve({ name: 'fastify', version: '<=3 <5' }, (fastify) => patcher.patch(fastify, {
78
78
  name: 'fastify',
79
79
  patchType,
80
80
  post(data) {
81
81
  const { result: server } = data;
82
82
  // Set our custom handler initially
83
- server.setErrorHandler(fastify3ErrorHandler.handler);
83
+ server.setErrorHandler(fastifyErrorHandler.handler);
84
84
 
85
85
  // Patch, so that if someone sets their own, we override with ours. But,
86
86
  // we do need to keep a reference to it so we can still call it for when
@@ -89,13 +89,13 @@ module.exports = function(core) {
89
89
  name: 'fastify.setErrorHandler',
90
90
  patchType,
91
91
  pre({ args }) {
92
- fastify3ErrorHandler._userHandler = args[0];
93
- args[0] = fastify3ErrorHandler.handler;
92
+ fastifyErrorHandler._userHandler = args[0];
93
+ args[0] = fastifyErrorHandler.handler;
94
94
  }
95
95
  });
96
96
  }
97
97
  }));
98
98
  };
99
99
 
100
- return fastify3ErrorHandler;
100
+ return fastifyErrorHandler;
101
101
  };
@@ -0,0 +1,75 @@
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 SecurityException = require('../../security-exception');
19
+ const { patchType } = require('../constants');
20
+
21
+
22
+ module.exports = function (core) {
23
+ const {
24
+ logger,
25
+ depHooks,
26
+ patcher,
27
+ protect,
28
+ } = core;
29
+
30
+ const hapiErrorHandler = protect.errorHandlers.hapiErrorHandler = {};
31
+
32
+ const registerErrorHandler = (boom, name) => {
33
+ patcher.patch(boom, 'boomify', {
34
+ name: `${name}.boomify`,
35
+ patchType,
36
+ post(data) {
37
+ const [err] = data.args;
38
+ const sourceContext = protect.getSourceContext('Hapi.boom.boomify');
39
+ const isSecurityException = SecurityException.isSecurityException(err);
40
+
41
+ if (isSecurityException && sourceContext && err.output.statusCode !== 403) {
42
+ const [mode, ruleId] = sourceContext.findings.securityException;
43
+ sourceContext.block('block', 'cmd-injection');
44
+
45
+ err.output.statusCode = 403;
46
+ err.reformat();
47
+ err.output.payload = undefined;
48
+ data.result = null;
49
+
50
+ logger.info({ mode, ruleId }, 'Request blocked');
51
+
52
+ return;
53
+ }
54
+
55
+ if (!sourceContext && isSecurityException) {
56
+ logger.info('source context not found; unable to handle response');
57
+ }
58
+ }
59
+ });
60
+ };
61
+
62
+ hapiErrorHandler.install = function () {
63
+ depHooks.resolve(
64
+ { name: 'boom' },
65
+ (boom) => registerErrorHandler(boom, 'boom'),
66
+ );
67
+
68
+ depHooks.resolve(
69
+ { name: '@hapi/boom' },
70
+ (boom) => registerErrorHandler(boom, '@hapi/boom'),
71
+ );
72
+ };
73
+
74
+ return hapiErrorHandler;
75
+ };
@@ -37,7 +37,7 @@ module.exports = function(core) {
37
37
 
38
38
  hardening.handleUntrustedDeserialization = function(sourceContext, sinkContext) {
39
39
  const ruleId = 'untrusted-deserialization';
40
- const { mode } = sourceContext.rules.agentRules[ruleId];
40
+ const mode = sourceContext.policy[ruleId];
41
41
  const { name, value } = sinkContext;
42
42
 
43
43
  if (mode === 'off') return;
package/lib/index.js CHANGED
@@ -16,20 +16,14 @@
16
16
  'use strict';
17
17
 
18
18
  const agentLib = require('@contrast/agent-lib');
19
+ const { installChildComponentsSync } = require('@contrast/common');
19
20
 
20
21
  module.exports = function(core) {
21
22
  const protect = core.protect = {
22
23
  agentLib: module.exports.instantiateAgentLib(agentLib),
23
24
  };
24
25
 
25
- const rules = instantiateRulesFromConfig(
26
- core.config.protect.rules,
27
- core.config.protect.disabled_rules,
28
- protect.agentLib,
29
- );
30
-
31
- protect.rules = rules;
32
-
26
+ require('./policy')(core);
33
27
  require('./throw-security-exception')(core);
34
28
  require('./make-response-blocker')(core);
35
29
  require('./make-source-context')(core);
@@ -44,10 +38,7 @@ module.exports = function(core) {
44
38
  protect.version = pkj.version;
45
39
 
46
40
  protect.install = function() {
47
- protect.inputAnalysis.install();
48
- protect.inputTracing.install();
49
- protect.hardening.install();
50
- protect.errorHandlers.install();
41
+ installChildComponentsSync(protect);
51
42
  };
52
43
 
53
44
  return protect;
@@ -71,38 +62,3 @@ function instantiateAgentLib(lib) {
71
62
  }
72
63
  return agentLib;
73
64
  }
74
-
75
- /**
76
- * This function instatiates the rules as defined in the configuration into
77
- * some structure. I'm in no way convinced or asserting that this is the right
78
- * structure but it does get a usable definition of rules in place. The final
79
- * structure will change based on what exactly TS sends as well as what the needs
80
- * of the code accessing the rules, exclusions, virtual-patches, etc.
81
- *
82
- * @param {Object} rules the rules object in the config.protect object.
83
- * @param {string[]} disabled array of disabled rules from config.protect
84
- * @param {Object} agentLib the agent-lib instance
85
- * @returns {Object} { agentLibRules, agentLibRulesMask, agentRules }
86
- */
87
- function instantiateRulesFromConfig(rules, disabled, agentLib) {
88
- const agentLibRules = {};
89
- let agentLibRulesMask = 0;
90
- const agentRules = {};
91
-
92
- for (const ruleId in rules) {
93
- if (disabled.indexOf(ruleId) >= 0 || rules[ruleId].mode === 'off') {
94
- continue;
95
- }
96
- // [matt] this is awkward. we should probably make each nosql-injection-x
97
- // rule separate in the config and only convert them to 'nosql-injection'
98
- // for reporting.
99
- if (agentLib.RuleType[ruleId]) {
100
- agentLibRules[ruleId] = rules[ruleId];
101
- agentLibRulesMask = agentLibRulesMask | agentLib.RuleType[ruleId];
102
- } else {
103
- agentRules[ruleId] = rules[ruleId];
104
- }
105
- }
106
-
107
- return { agentLibRules, agentLibRulesMask, agentRules };
108
- }
@@ -17,6 +17,7 @@
17
17
 
18
18
  const { BLOCKING_MODES, simpleTraverse } = require('@contrast/common');
19
19
  const address = require('ipaddr.js');
20
+ const { Rule } = require('@contrast/common');
20
21
 
21
22
  //
22
23
  // these rules are not implemented by agent-lib, but are being considered for
@@ -54,6 +55,7 @@ module.exports = function(core) {
54
55
  agentLib,
55
56
  inputAnalysis,
56
57
  },
58
+ config,
57
59
  } = core;
58
60
 
59
61
  // all handlers will be invoked with two arguments:
@@ -113,26 +115,82 @@ module.exports = function(core) {
113
115
  inputAnalysis.handleConnect = function handleConnect(sourceContext, connectInputs) {
114
116
  if (!sourceContext || sourceContext.allowed) return;
115
117
 
116
- const { rules: { agentLibRules, agentLibRulesMask: mask } } = sourceContext;
118
+ const { policy: { rulesMask } } = sourceContext;
117
119
 
118
120
  inputAnalysis.handleVirtualPatches(sourceContext, { URLS: connectInputs.rawUrl, HEADERS: connectInputs.headers });
119
121
 
120
122
  // initialize findings to the basics
121
123
  let block = undefined;
122
- if (mask !== 0) {
123
- const findings = agentLib.scoreRequestConnect(mask, connectInputs, preferWW);
124
- block = mergeFindings(agentLibRules, sourceContext.findings, findings);
124
+ if (rulesMask !== 0) {
125
+ const findings = agentLib.scoreRequestConnect(rulesMask, connectInputs, preferWW);
126
+ block = mergeFindings(sourceContext, findings);
125
127
  }
126
128
 
127
129
  return block;
128
130
  };
129
131
 
130
- // this is called before the request goes away. this is where probe detection takes
131
- // place. basically loop through findings.resultsList and, for all those that weren't
132
- // blocked, run scoreAtom() with preferWorthWatching: false. for any that have a score
133
- // >= 90 report them as probes.
132
+ /**
133
+ * handleRequestEnd()
134
+ *
135
+ * Invoked when the request is complete.
136
+ *
137
+ * @param {Object} sourceContext
138
+ */
134
139
  inputAnalysis.handleRequestEnd = function handleRequestEnd(sourceContext) {
135
- throw new Error('nyi', sourceContext);
140
+ if (!config.protect.probe_analysis.enable) return;
141
+
142
+ const { resultsMap } = sourceContext.findings;
143
+ const probesRules = [Rule.CMD_INJECTION, Rule.PATH_TRAVERSAL, Rule.SQL_INJECTION, Rule.XXE];
144
+ const props = {};
145
+
146
+ // Detecting probes
147
+ Object.values(resultsMap).forEach(resultsByRuleId => {
148
+ resultsByRuleId.forEach((resultByRuleId) => {
149
+ const {
150
+ ruleId,
151
+ blocked,
152
+ details,
153
+ value,
154
+ inputType
155
+ } = resultByRuleId;
156
+ if (blocked || !blocked && details.length > 0 || !probesRules.some(rule => rule === ruleId)) return;
157
+
158
+ const { policy: { rulesMask } } = sourceContext;
159
+
160
+ const results = (agentLib.scoreAtom(
161
+ rulesMask,
162
+ value,
163
+ agentLib.InputType[inputType],
164
+ {
165
+ preferWorthWatching: false
166
+ }
167
+ ) || []).filter(({ score }) => score >= 90);
168
+
169
+ if (!results.length) return;
170
+
171
+ results.forEach(result => {
172
+ const isAlreadyBlocked = (resultsMap[result.ruleId] || []).some(element =>
173
+ element.blocked && element.inputType === inputType && element.value === value
174
+ );
175
+
176
+ if (isAlreadyBlocked) return;
177
+
178
+ const probe = Object.assign({}, resultByRuleId, result, {
179
+ mappedId: result.ruleId
180
+ });
181
+ const key = [probe.ruleId, probe.inputType, ...probe.path, probe.value].join('|');
182
+ props[key] = probe;
183
+ });
184
+ });
185
+ });
186
+
187
+ Object.values(props).forEach(prop => {
188
+ if (!resultsMap[prop.ruleId]) {
189
+ resultsMap[prop.ruleId] = [];
190
+ }
191
+
192
+ resultsMap[prop.ruleId].push(prop);
193
+ });
136
194
  };
137
195
 
138
196
  const jsonInputTypes = {
@@ -198,7 +256,7 @@ module.exports = function(core) {
198
256
 
199
257
  inputAnalysis.handleVirtualPatches(sourceContext, { PARAMETERS: urlParams });
200
258
 
201
- const { rules, rules: { agentLibRulesMask: mask } } = sourceContext;
259
+ const { policy: { rulesMask } } = sourceContext;
202
260
  const resultsList = [];
203
261
  const { UrlParameter } = agentLib.InputType;
204
262
 
@@ -207,7 +265,7 @@ module.exports = function(core) {
207
265
  if (type !== 'Value') {
208
266
  return;
209
267
  }
210
- const items = agentLib.scoreAtom(mask, value, UrlParameter, preferWW);
268
+ const items = agentLib.scoreAtom(rulesMask, value, UrlParameter, preferWW);
211
269
  if (!items) {
212
270
  return;
213
271
  }
@@ -236,7 +294,7 @@ module.exports = function(core) {
236
294
  resultsList,
237
295
  };
238
296
 
239
- const block = mergeFindings(rules.agentLibRules, sourceContext.findings, urlParamsFindings);
297
+ const block = mergeFindings(sourceContext, urlParamsFindings);
240
298
 
241
299
  if (block) {
242
300
  core.protect.throwSecurityException(sourceContext);
@@ -262,10 +320,10 @@ module.exports = function(core) {
262
320
 
263
321
  inputAnalysis.handleVirtualPatches(sourceContext, { HEADERS: cookies });
264
322
 
265
- const { rules, rules: { agentLibRulesMask: mask } } = sourceContext;
266
- const cookieFindings = agentLib.scoreRequestConnect(mask, { cookies: cookiesArr }, preferWW);
323
+ const { policy: { rulesMask } } = sourceContext;
324
+ const cookieFindings = agentLib.scoreRequestConnect(rulesMask, { cookies: cookiesArr }, preferWW);
267
325
 
268
- const block = mergeFindings(rules.agentLibRules, sourceContext.findings, cookieFindings);
326
+ const block = mergeFindings(sourceContext, cookieFindings);
269
327
 
270
328
  if (block) {
271
329
  core.protect.throwSecurityException(sourceContext);
@@ -397,11 +455,12 @@ module.exports = function(core) {
397
455
  * @returns {Array | undefined} returns an array with block info if vulnerability was found.
398
456
  */
399
457
  function commonObjectAnalyzer(sourceContext, object, inputTypes) {
400
- const { rules, rules: { agentLibRulesMask: mask } } = sourceContext;
458
+ const { policy: { rulesMask } } = sourceContext;
459
+ if (!rulesMask) return;
460
+
401
461
  // use inputTypes to set params...
402
462
  const { keyType, inputType } = inputTypes;
403
463
  const inputTypeStr = inputTypes === jsonInputTypes ? 'Json' : 'Parameter';
404
- const { Where } = agentLib.MongoQueryType;
405
464
  const resultsList = [];
406
465
 
407
466
  // it's possible to optimize this if qs (or a similar package) is not loaded
@@ -420,7 +479,7 @@ module.exports = function(core) {
420
479
  /* eslint-disable-next-line complexity */
421
480
  simpleTraverse(object, function(path, type, value) {
422
481
  let itemType;
423
- let mongoQueryType;
482
+ let isMongoQueryType;
424
483
  // this is a bit awkward now because nosql-injection-mongo is not integrated
425
484
  // into the scoreAtom() function (or the check_input() function it uses). as
426
485
  // a result, the two rules need to be checked independently and the results
@@ -428,14 +487,14 @@ module.exports = function(core) {
428
487
  // TODO AGENT-205
429
488
  if (type === 'Key') {
430
489
  itemType = keyType;
431
- if (mask & agentLib.RuleType['nosql-injection-mongo']) {
432
- mongoQueryType = agentLib.getMongoQueryType(value);
490
+ if (rulesMask & agentLib.RuleType['nosql-injection-mongo']) {
491
+ isMongoQueryType = agentLib.isMongoQueryType(value);
433
492
  }
434
493
  } else {
435
494
  itemType = inputType;
436
495
  }
437
- let items = agentLib.scoreAtom(mask, value, itemType, preferWW);
438
- if (!items && !mongoQueryType) {
496
+ let items = agentLib.scoreAtom(rulesMask, value, itemType, preferWW);
497
+ if (!items && !isMongoQueryType) {
439
498
  return;
440
499
  }
441
500
  if (!items) {
@@ -444,18 +503,13 @@ module.exports = function(core) {
444
503
  let mongoPath;
445
504
  // if the key was a mongo query key, then add it to the items. it requires
446
505
  // that additional information is kept as well.
447
- if (mongoQueryType) {
506
+ if (isMongoQueryType) {
448
507
  const inputToCheck = getValueAtKey(object, path, value);
449
508
  // because scoreRequestConnect() returns the query type in the value, we
450
509
  // mimic it here (where scoreAtom() was used). the actual object/string
451
510
  // to match is stored as `inputToCheck`.
452
- const inputType = typeof inputToCheck;
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') {
455
- // the query-type/input-type combination is valid. add a synthesized item.
456
- const item = { ruleId: 'nosql-injection-mongo', score: 10, mongoContext: { inputToCheck } };
457
- items.push(item);
458
- }
511
+ const item = { ruleId: 'nosql-injection-mongo', score: 10, mongoContext: { inputToCheck } };
512
+ items.push(item);
459
513
  }
460
514
  // make each item a complete Finding
461
515
  for (const item of items) {
@@ -464,8 +518,7 @@ module.exports = function(core) {
464
518
  inputType: `${inputTypeStr}${type}`,
465
519
  path: mongoPath || path.slice(),
466
520
  key: type === 'Key' ? value : path[path.length - 1],
467
- // mimic scoreRequestConnect() returning the query type as the value
468
- value: mongoQueryType || value,
521
+ value,
469
522
  score: item.score,
470
523
  idsList: [],
471
524
  };
@@ -488,7 +541,7 @@ module.exports = function(core) {
488
541
  resultsList,
489
542
  };
490
543
 
491
- return mergeFindings(rules.agentLibRules, sourceContext.findings, findings);
544
+ return mergeFindings(sourceContext, findings);
492
545
  }
493
546
 
494
547
  function ipListAnalysis(reqIp, reqHeaders, list) {
@@ -543,11 +596,13 @@ module.exports = function(core) {
543
596
  * @param {Object} newFindings the findings, in {trackRequest, resultsList} format.
544
597
  * @returns {undefined|[String]} undefined to permit else [mode, rule] to block.
545
598
  */
546
- function mergeFindings(rules, findings, newFindings) {
599
+ function mergeFindings(sourceContext, newFindings) {
600
+ const { findings, policy } = sourceContext;
601
+
547
602
  if (!newFindings.trackRequest) {
548
603
  return findings.securityException;
549
604
  }
550
- normalizeFindings(rules, newFindings);
605
+ normalizeFindings(policy, newFindings);
551
606
 
552
607
  findings.trackRequest = findings.trackRequest || newFindings.trackRequest;
553
608
  findings.securityException = findings.securityException || newFindings.securityException;
@@ -566,7 +621,7 @@ function mergeFindings(rules, findings, newFindings) {
566
621
  //
567
622
  // add common fields to findings.
568
623
  //
569
- function normalizeFindings(rules, findings) {
624
+ function normalizeFindings(policy, findings) {
570
625
  // now both augment the rules and check to see if any require blocking
571
626
  // at perimeter.
572
627
  for (const r of findings.resultsList) {
@@ -590,7 +645,7 @@ function normalizeFindings(rules, findings) {
590
645
  // option and that implies that there is no sink, so this is the only place at
591
646
  // which the block can occur. so at a minimum 'block' should also result in a
592
647
  // block.
593
- const { mode } = rules[r.ruleId];
648
+ const mode = policy[r.ruleId];
594
649
  if (r.score >= 90 && BLOCKING_MODES.includes(mode)) {
595
650
  r.blocked = true;
596
651
  findings.securityException = [mode, r.ruleId];
@@ -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,20 +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);
41
44
 
42
45
  // virtual patches
43
46
  require('./virtual-patches')(core);
44
47
  require('./ip-analysis')(core);
45
48
 
46
49
  inputAnalysis.install = function() {
47
- Object.values(inputAnalysis)
48
- .filter((property) => property.install)
49
- .forEach((library) => {
50
- library.install();
51
- });
50
+ installChildComponentsSync(inputAnalysis);
52
51
  };
53
52
 
54
53
  return inputAnalysis;
@@ -33,8 +33,18 @@ module.exports = (core) => {
33
33
  if (sourceContext && req.body && Object.keys(req.body).length) {
34
34
  sourceContext.parsedBody = req.body;
35
35
 
36
+ if (fnName === 'bodyParser.text' && typeof req.body === 'string') {
37
+ try {
38
+ sourceContext.parsedBody = JSON.parse(req.body);
39
+ } catch (err) {
40
+ logger.error({ err }, 'Error parsing with bodyParser.text()');
41
+ origNext();
42
+ return;
43
+ }
44
+ }
45
+
36
46
  try {
37
- inputAnalysis.handleParsedBody(sourceContext, req.body);
47
+ inputAnalysis.handleParsedBody(sourceContext, sourceContext.parsedBody);
38
48
  } catch (err) {
39
49
  if (isSecurityException(err)) {
40
50
  securityException = err;
@@ -0,0 +1,84 @@
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) => patcher.patch(fastify, {
40
+ name: 'fastify.build',
41
+ patchType,
42
+ post({ result: server }) {
43
+ server.addHook('preValidation', function(request, reply, done) {
44
+ let securityException;
45
+ const sourceContext = protect.getSourceContext('Fastify.preValidationHook');
46
+
47
+ if (sourceContext) {
48
+ try {
49
+ if (request.params) {
50
+ sourceContext.parsedParams = request.params;
51
+ inputAnalysis.handleUrlParams(sourceContext, request.params);
52
+ }
53
+ if (request.cookies) {
54
+ sourceContext.parsedCookies = request.cookies;
55
+ inputAnalysis.handleCookies(sourceContext, request.cookies);
56
+ }
57
+ if (request.body) {
58
+ sourceContext.parsedBody = request.body;
59
+ inputAnalysis.handleParsedBody(sourceContext, request.body);
60
+ }
61
+
62
+ if (request.query) {
63
+ sourceContext.parsedQuery = request.query;
64
+ inputAnalysis.handleQueryParams(sourceContext, request.query);
65
+ }
66
+ } catch (err) {
67
+ if (isSecurityException(err)) {
68
+ securityException = err;
69
+ } else {
70
+ logger.error({ err }, 'Unexpected error during input analysis');
71
+ }
72
+ }
73
+ }
74
+
75
+ done(securityException);
76
+ });
77
+ },
78
+ }));
79
+ }
80
+
81
+ return inputAnalysis.fastifyInstrumentation = {
82
+ install,
83
+ };
84
+ };