@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.
- package/lib/error-handlers/index.js +7 -4
- package/lib/error-handlers/install/{fastify3.js → fastify.js} +12 -12
- package/lib/error-handlers/install/hapi.js +75 -0
- package/lib/hardening/handlers.js +1 -1
- package/lib/index.js +3 -47
- package/lib/input-analysis/handlers.js +92 -37
- package/lib/input-analysis/index.js +5 -6
- package/lib/input-analysis/install/body-parser1.js +11 -1
- package/lib/input-analysis/install/fastify.js +84 -0
- package/lib/input-analysis/install/hapi.js +106 -0
- package/lib/input-analysis/install/http.js +74 -31
- package/lib/input-tracing/handlers/index.js +8 -3
- package/lib/input-tracing/index.js +9 -19
- package/lib/input-tracing/install/child-process.js +1 -0
- package/lib/input-tracing/install/eval.js +3 -3
- package/lib/input-tracing/install/fs.js +1 -0
- package/lib/input-tracing/install/function.js +62 -0
- package/lib/make-source-context.js +4 -48
- package/lib/policy.js +134 -0
- package/lib/semantic-analysis/handlers.js +43 -25
- package/package.json +5 -5
- package/lib/input-analysis/install/fastify3.js +0 -106
|
@@ -18,14 +18,17 @@
|
|
|
18
18
|
module.exports = function(core) {
|
|
19
19
|
const errorHandlers = core.protect.errorHandlers = {};
|
|
20
20
|
|
|
21
|
-
require('./install/
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
57
|
-
const normalHandler =
|
|
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('
|
|
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
|
-
|
|
77
|
-
depHooks.resolve({ name: 'fastify', version: '<=
|
|
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(
|
|
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
|
-
|
|
93
|
-
args[0] =
|
|
92
|
+
fastifyErrorHandler._userHandler = args[0];
|
|
93
|
+
args[0] = fastifyErrorHandler.handler;
|
|
94
94
|
}
|
|
95
95
|
});
|
|
96
96
|
}
|
|
97
97
|
}));
|
|
98
98
|
};
|
|
99
99
|
|
|
100
|
-
return
|
|
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
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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 (
|
|
123
|
-
const findings = agentLib.scoreRequestConnect(
|
|
124
|
-
block = mergeFindings(
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
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 {
|
|
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(
|
|
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(
|
|
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 {
|
|
266
|
-
const cookieFindings = agentLib.scoreRequestConnect(
|
|
323
|
+
const { policy: { rulesMask } } = sourceContext;
|
|
324
|
+
const cookieFindings = agentLib.scoreRequestConnect(rulesMask, { cookies: cookiesArr }, preferWW);
|
|
267
325
|
|
|
268
|
-
const block = mergeFindings(
|
|
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 {
|
|
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
|
|
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 (
|
|
432
|
-
|
|
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(
|
|
438
|
-
if (!items && !
|
|
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 (
|
|
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
|
|
453
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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/
|
|
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
|
-
|
|
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,
|
|
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
|
+
};
|