@contrast/protect 1.20.0 → 1.22.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/get-source-context.js +2 -2
- package/lib/hardening/handlers.js +6 -2
- package/lib/input-analysis/handlers.js +2 -2
- package/lib/input-analysis/install/express4.js +63 -46
- package/lib/input-tracing/handlers/index.js +8 -7
- package/lib/semantic-analysis/install/libxmljs.js +52 -44
- package/package.json +4 -4
|
@@ -15,14 +15,14 @@
|
|
|
15
15
|
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
|
-
module.exports = function(core) {
|
|
18
|
+
module.exports = function (core) {
|
|
19
19
|
const { scopes: { sources }, logger } = core;
|
|
20
20
|
|
|
21
21
|
function getSourceContext(callPoint) {
|
|
22
22
|
const sourceContext = sources.getStore()?.protect;
|
|
23
23
|
|
|
24
24
|
if (!sourceContext) {
|
|
25
|
-
logger.debug(
|
|
25
|
+
logger.debug('source context not available in %s', callPoint);
|
|
26
26
|
return null;
|
|
27
27
|
}
|
|
28
28
|
|
|
@@ -15,7 +15,11 @@
|
|
|
15
15
|
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
|
-
const {
|
|
18
|
+
const {
|
|
19
|
+
BLOCKING_MODES,
|
|
20
|
+
isString,
|
|
21
|
+
Rule: { UNTRUSTED_DESERIALIZATION }
|
|
22
|
+
} = require('@contrast/common');
|
|
19
23
|
|
|
20
24
|
const NODE_SERIALIZE_RCE_TOKEN = '_$$ND_FUNC$$_';
|
|
21
25
|
|
|
@@ -37,7 +41,7 @@ module.exports = function(core) {
|
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
hardening.handleUntrustedDeserialization = function(sourceContext, sinkContext) {
|
|
40
|
-
const ruleId =
|
|
44
|
+
const ruleId = UNTRUSTED_DESERIALIZATION;
|
|
41
45
|
const mode = sourceContext.policy[ruleId];
|
|
42
46
|
const { name, value, stacktraceOpts } = sinkContext;
|
|
43
47
|
|
|
@@ -398,7 +398,7 @@ module.exports = function(core) {
|
|
|
398
398
|
};
|
|
399
399
|
|
|
400
400
|
inputAnalysis.handleVirtualPatches = function(sourceContext, requestInput) {
|
|
401
|
-
const ruleId =
|
|
401
|
+
const ruleId = Rule.VIRTUAL_PATCH;
|
|
402
402
|
|
|
403
403
|
if (!Object.keys(requestInput).filter(Boolean).length || !sourceContext?.virtualPatchesEvaluators.length) return;
|
|
404
404
|
|
|
@@ -440,7 +440,7 @@ module.exports = function(core) {
|
|
|
440
440
|
};
|
|
441
441
|
|
|
442
442
|
inputAnalysis.handleIpDenylist = function(sourceContext, ipDenylist) {
|
|
443
|
-
const ruleId =
|
|
443
|
+
const ruleId = Rule.IP_DENYLIST;
|
|
444
444
|
|
|
445
445
|
if (!sourceContext || !ipDenylist.length) return;
|
|
446
446
|
|
|
@@ -36,63 +36,80 @@ module.exports = (core) => {
|
|
|
36
36
|
* registers a depHook for express module instrumentation
|
|
37
37
|
*/
|
|
38
38
|
function install() {
|
|
39
|
-
depHooks.resolve(
|
|
40
|
-
name: '
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
39
|
+
depHooks.resolve(
|
|
40
|
+
{ name: 'express', version: '>=4.0.0 <5.0.0', file: 'lib/middleware/query.js' },
|
|
41
|
+
(query) => patcher.patch(query, {
|
|
42
|
+
name: 'express.query',
|
|
43
|
+
patchType,
|
|
44
|
+
post(data) {
|
|
45
|
+
data.result = patcher.patch(data.result, {
|
|
46
|
+
name: 'express.query',
|
|
47
|
+
patchType,
|
|
48
|
+
pre(data) {
|
|
49
|
+
const [req, , origNext] = data.args;
|
|
50
|
+
|
|
51
|
+
function contrastNext(origErr) {
|
|
52
|
+
const sourceContext = protect.getSourceContext('express.query');
|
|
53
|
+
let securityException;
|
|
54
|
+
|
|
55
|
+
// It is possible for the query to be already parsed by `qs`
|
|
56
|
+
// which means that we've already handled/analyzed it.
|
|
57
|
+
// So we check whether we already have the `parsedQuery` property in the context
|
|
58
|
+
if (sourceContext && req.query && Object.keys(req.query).length && (!('parsedQuery' in sourceContext))) {
|
|
59
|
+
sourceContext.parsedQuery = req.query;
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
inputAnalysis.handleQueryParams(sourceContext, req.query);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
if (isSecurityException(err)) {
|
|
65
|
+
securityException = err;
|
|
66
|
+
} else {
|
|
67
|
+
logger.error({ err }, 'Unexpected error during input analysis');
|
|
68
|
+
}
|
|
66
69
|
}
|
|
67
70
|
}
|
|
71
|
+
|
|
72
|
+
const error = securityException || origErr;
|
|
73
|
+
|
|
74
|
+
origNext(error);
|
|
68
75
|
}
|
|
69
76
|
|
|
70
|
-
|
|
77
|
+
data.args[2] = contrastNext;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
depHooks.resolve(
|
|
84
|
+
{ name: 'express', version: '>=4.0.0 <5.0.0', file: 'lib/router/layer.js' },
|
|
85
|
+
(Layer) => {
|
|
86
|
+
const name = 'express.Layer.prototype.match';
|
|
87
|
+
patcher.patch(Layer.prototype, 'match', {
|
|
88
|
+
name,
|
|
89
|
+
patchType,
|
|
90
|
+
post(data) {
|
|
91
|
+
const layer = data.obj;
|
|
71
92
|
|
|
72
|
-
|
|
93
|
+
// we can exit early if
|
|
94
|
+
// the layer doesn't match the request or
|
|
95
|
+
// the layer doesn't recognize any parameters
|
|
96
|
+
if (!data.result || !layer.keys || layer.keys.length === 0) {
|
|
97
|
+
return;
|
|
73
98
|
}
|
|
74
99
|
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
}));
|
|
100
|
+
const sourceContext = protect.getSourceContext(name);
|
|
80
101
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
patchType,
|
|
85
|
-
pre(data) {
|
|
86
|
-
const { obj: { params } } = data;
|
|
87
|
-
const sourceContext = protect.getSourceContext('Express.Layer.handle_request');
|
|
102
|
+
if (!sourceContext) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
88
105
|
|
|
89
|
-
|
|
90
|
-
sourceContext.
|
|
91
|
-
inputAnalysis.handleUrlParams(sourceContext, params);
|
|
106
|
+
sourceContext.parsedParams = layer.params;
|
|
107
|
+
inputAnalysis.handleUrlParams(sourceContext, layer.params);
|
|
92
108
|
}
|
|
93
|
-
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
return Layer;
|
|
94
112
|
});
|
|
95
|
-
});
|
|
96
113
|
}
|
|
97
114
|
|
|
98
115
|
const express4Instrumentation = inputAnalysis.express4Instrumentation = {
|
|
@@ -22,7 +22,8 @@ const {
|
|
|
22
22
|
isString,
|
|
23
23
|
traverseKeys,
|
|
24
24
|
traverseKeysAndValues,
|
|
25
|
-
agentLibIDListTypes
|
|
25
|
+
agentLibIDListTypes,
|
|
26
|
+
Rule: { SQL_INJECTION, PATH_TRAVERSAL, CMD_INJECTION, NOSQL_INJECTION_MONGO, SSJS_INJECTION, REFLECTED_XSS }
|
|
26
27
|
} = require('@contrast/common');
|
|
27
28
|
|
|
28
29
|
module.exports = function(core) {
|
|
@@ -51,7 +52,7 @@ module.exports = function(core) {
|
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
inputTracing.handlePathTraversal = function(sourceContext, sinkContext) {
|
|
54
|
-
const ruleId =
|
|
55
|
+
const ruleId = PATH_TRAVERSAL;
|
|
55
56
|
const results = getResultsByRuleId(ruleId, sourceContext);
|
|
56
57
|
|
|
57
58
|
if (!results) return;
|
|
@@ -67,7 +68,7 @@ module.exports = function(core) {
|
|
|
67
68
|
};
|
|
68
69
|
|
|
69
70
|
inputTracing.handleCommandInjection = function(sourceContext, sinkContext) {
|
|
70
|
-
const ruleId =
|
|
71
|
+
const ruleId = CMD_INJECTION;
|
|
71
72
|
const results = getResultsByRuleId(ruleId, sourceContext);
|
|
72
73
|
|
|
73
74
|
if (!results) return;
|
|
@@ -93,7 +94,7 @@ module.exports = function(core) {
|
|
|
93
94
|
};
|
|
94
95
|
|
|
95
96
|
inputTracing.handleSqlInjection = function(sourceContext, sinkContext) {
|
|
96
|
-
const ruleId =
|
|
97
|
+
const ruleId = SQL_INJECTION;
|
|
97
98
|
const results = getResultsByRuleId(ruleId, sourceContext);
|
|
98
99
|
|
|
99
100
|
if (!results) return;
|
|
@@ -129,7 +130,7 @@ module.exports = function(core) {
|
|
|
129
130
|
};
|
|
130
131
|
|
|
131
132
|
inputTracing.nosqlInjectionMongo = function (sourceContext, sinkContext) {
|
|
132
|
-
const ruleId =
|
|
133
|
+
const ruleId = NOSQL_INJECTION_MONGO;
|
|
133
134
|
const nosqlInjectionMongoResults =
|
|
134
135
|
getResultsByRuleId(ruleId, sourceContext) || [];
|
|
135
136
|
const ssjsInjectionResults =
|
|
@@ -238,7 +239,7 @@ module.exports = function(core) {
|
|
|
238
239
|
};
|
|
239
240
|
|
|
240
241
|
inputTracing.ssjsInjection = function(sourceContext, sinkContext) {
|
|
241
|
-
const ruleId =
|
|
242
|
+
const ruleId = SSJS_INJECTION;
|
|
242
243
|
let sinkValuesArr = [];
|
|
243
244
|
|
|
244
245
|
const results = getResultsByRuleId(ruleId, sourceContext);
|
|
@@ -289,7 +290,7 @@ module.exports = function(core) {
|
|
|
289
290
|
};
|
|
290
291
|
|
|
291
292
|
inputTracing.handleReflectedXss = function(sourceContext, sinkContext) {
|
|
292
|
-
const ruleId =
|
|
293
|
+
const ruleId = REFLECTED_XSS;
|
|
293
294
|
const results = getResultsByRuleId(ruleId, sourceContext);
|
|
294
295
|
|
|
295
296
|
if (!results) return;
|
|
@@ -19,64 +19,72 @@ const { isString } = require('@contrast/common');
|
|
|
19
19
|
const SecurityException = require('../../security-exception');
|
|
20
20
|
const { patchType } = require('../constants');
|
|
21
21
|
|
|
22
|
-
module.exports = function(core) {
|
|
22
|
+
module.exports = function (core) {
|
|
23
23
|
const {
|
|
24
24
|
depHooks,
|
|
25
25
|
patcher,
|
|
26
26
|
logger,
|
|
27
|
-
protect: { semanticAnalysis },
|
|
28
27
|
protect,
|
|
29
28
|
} = core;
|
|
30
29
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
30
|
+
/**
|
|
31
|
+
* @param {boolean} newApi
|
|
32
|
+
*/
|
|
33
|
+
const handler = (newApi) => (mod, metadata) => {
|
|
34
|
+
const checkOptions = newApi
|
|
35
|
+
? (opts) => !opts?.noent && !opts?.replaceEntities
|
|
36
|
+
: (opts) => !opts?.noent;
|
|
38
37
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
patchType,
|
|
43
|
-
pre: preParseXmlMethod
|
|
44
|
-
});
|
|
38
|
+
const methods = newApi
|
|
39
|
+
? ['parseXml', 'parseXmlAsync']
|
|
40
|
+
: ['parseXml', 'parseXmlString'];
|
|
45
41
|
|
|
46
|
-
|
|
47
|
-
name
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
42
|
+
methods.forEach((method) => {
|
|
43
|
+
const name = `${metadata.name}:${method}`;
|
|
44
|
+
patcher.patch(mod, method, {
|
|
45
|
+
name,
|
|
46
|
+
patchType,
|
|
47
|
+
pre({ args, hooked, orig }) {
|
|
48
|
+
const sourceContext = protect.getSourceContext(name);
|
|
49
|
+
const [value, options] = args;
|
|
52
50
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
if (!sourceContext || !value || !isString(value) || checkOptions(options)) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
56
54
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
55
|
+
const sinkContext = {
|
|
56
|
+
name,
|
|
57
|
+
value,
|
|
58
|
+
stacktraceOpts: {
|
|
59
|
+
constructorOpt: hooked,
|
|
60
|
+
prependFrames: [orig]
|
|
61
|
+
},
|
|
62
|
+
};
|
|
61
63
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
try {
|
|
65
|
+
protect.semanticAnalysis.handleXXE(sourceContext, sinkContext);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
if (SecurityException.isSecurityException(err)) {
|
|
68
|
+
throw err;
|
|
69
|
+
}
|
|
67
70
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
logger.error({ err }, 'Unexpected error during semantic analysis');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
};
|
|
74
77
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
+
protect.semanticAnalysis.libxmljs = {
|
|
79
|
+
install() {
|
|
80
|
+
// libxmljs changed its API in version 1.0.0
|
|
81
|
+
depHooks.resolve({ name: 'libxmljs', version: '>=1' }, handler(true));
|
|
78
82
|
|
|
79
|
-
|
|
83
|
+
// libxmljs versions prior to 1.0.0 and libxmljs2 share the same API
|
|
84
|
+
depHooks.resolve({ name: 'libxmljs', version: '<1' }, handler(false));
|
|
85
|
+
depHooks.resolve({ name: 'libxmljs2' }, handler(false));
|
|
86
|
+
}
|
|
87
|
+
};
|
|
80
88
|
|
|
81
|
-
return libxmljs;
|
|
89
|
+
return protect.semanticAnalysis.libxmljs;
|
|
82
90
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contrast/protect",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.22.0",
|
|
4
4
|
"description": "Contrast service providing framework-agnostic Protect support",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"author": "Contrast Security <nodejs@contrastsecurity.com> (https://www.contrastsecurity.com)",
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@contrast/agent-lib": "^7.0.1",
|
|
21
|
-
"@contrast/common": "1.
|
|
22
|
-
"@contrast/core": "1.
|
|
23
|
-
"@contrast/esm-hooks": "1.
|
|
21
|
+
"@contrast/common": "1.13.0",
|
|
22
|
+
"@contrast/core": "1.20.0",
|
|
23
|
+
"@contrast/esm-hooks": "1.16.0",
|
|
24
24
|
"@contrast/scopes": "1.4.0",
|
|
25
25
|
"ipaddr.js": "^2.0.1",
|
|
26
26
|
"semver": "^7.3.7"
|