@contrast/assess 1.8.0 → 1.10.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/dataflow/event-factory.js +17 -13
- package/lib/dataflow/propagation/index.js +4 -0
- package/lib/dataflow/propagation/install/JSON/index.js +1 -0
- package/lib/dataflow/propagation/install/JSON/parse-fn.js +248 -0
- package/lib/dataflow/propagation/install/JSON/parse.js +196 -0
- package/lib/dataflow/propagation/install/JSON/stringify.js +5 -3
- package/lib/dataflow/propagation/install/array-prototype-join.js +21 -14
- package/lib/dataflow/propagation/install/buffer.js +2 -0
- package/lib/dataflow/propagation/install/contrast-methods/add.js +2 -0
- package/lib/dataflow/propagation/install/contrast-methods/index.js +1 -0
- package/lib/dataflow/propagation/install/contrast-methods/number.js +58 -0
- package/lib/dataflow/propagation/install/contrast-methods/string.js +53 -6
- package/lib/dataflow/propagation/install/contrast-methods/tag.js +3 -1
- package/lib/dataflow/propagation/install/decode-uri-component.js +9 -2
- package/lib/dataflow/propagation/install/ejs/escape-xml.js +8 -2
- package/lib/dataflow/propagation/install/encode-uri-component.js +9 -2
- package/lib/dataflow/propagation/install/escape-html.js +13 -5
- package/lib/dataflow/propagation/install/escape.js +9 -2
- package/lib/dataflow/propagation/install/handlebars-utils-escape-expression.js +11 -4
- package/lib/dataflow/propagation/install/isnumeric-0.js +59 -0
- package/lib/dataflow/propagation/install/mongoose/index.js +36 -0
- package/lib/dataflow/propagation/install/mongoose/schema-string.js +156 -0
- package/lib/dataflow/propagation/install/mysql-connection-escape.js +5 -0
- package/lib/dataflow/propagation/install/parse-int.js +60 -0
- package/lib/dataflow/propagation/install/path/basename.js +124 -0
- package/lib/dataflow/propagation/install/path/common.js +176 -0
- package/lib/dataflow/propagation/install/path/index.js +32 -0
- package/lib/dataflow/propagation/install/path/join-and-resolve.js +141 -0
- package/lib/dataflow/propagation/install/path/normalize.js +123 -0
- package/lib/dataflow/propagation/install/pug-runtime-escape.js +9 -2
- package/lib/dataflow/propagation/install/querystring/parse.js +12 -10
- package/lib/dataflow/propagation/install/sequelize.js +6 -3
- package/lib/dataflow/propagation/install/sql-template-strings.js +9 -2
- package/lib/dataflow/propagation/install/string/concat.js +8 -2
- package/lib/dataflow/propagation/install/string/format-methods.js +7 -2
- package/lib/dataflow/propagation/install/string/html-methods.js +15 -5
- package/lib/dataflow/propagation/install/string/match.js +16 -11
- package/lib/dataflow/propagation/install/string/replace.js +23 -15
- package/lib/dataflow/propagation/install/string/slice.js +14 -6
- package/lib/dataflow/propagation/install/string/split.js +16 -12
- package/lib/dataflow/propagation/install/string/substring.js +18 -8
- package/lib/dataflow/propagation/install/string/trim.js +4 -1
- package/lib/dataflow/propagation/install/unescape.js +9 -2
- package/lib/dataflow/propagation/install/url/domain-parsers.js +7 -2
- package/lib/dataflow/propagation/install/url/index.js +1 -0
- package/lib/dataflow/propagation/install/url/url.js +228 -0
- package/lib/dataflow/propagation/install/validator/hooks.js +6 -2
- package/lib/dataflow/sinks/index.js +8 -4
- package/lib/dataflow/sinks/install/child-process.js +116 -50
- package/lib/dataflow/sinks/install/eval.js +138 -0
- package/lib/dataflow/sinks/install/express/unvalidated-redirect.js +7 -4
- package/lib/dataflow/sinks/install/fastify/unvalidated-redirect.js +9 -5
- package/lib/dataflow/sinks/install/fs.js +45 -13
- package/lib/dataflow/sinks/install/function.js +160 -0
- package/lib/dataflow/sinks/install/http/index.js +31 -0
- package/lib/dataflow/sinks/install/http/request.js +152 -0
- package/lib/dataflow/sinks/install/{http.js → http/server-response.js} +7 -4
- package/lib/dataflow/sinks/install/koa/unvalidated-redirect.js +8 -5
- package/lib/dataflow/sinks/install/marsdb.js +3 -0
- package/lib/dataflow/sinks/install/mongodb.js +7 -24
- package/lib/dataflow/sinks/install/mssql.js +49 -29
- package/lib/dataflow/sinks/install/mysql.js +9 -4
- package/lib/dataflow/sinks/install/postgres.js +6 -3
- package/lib/dataflow/sinks/install/sequelize.js +7 -5
- package/lib/dataflow/sinks/install/sqlite3.js +7 -3
- package/lib/dataflow/sinks/install/vm.js +276 -0
- package/lib/dataflow/sources/handler.js +2 -1
- package/lib/dataflow/sources/install/http.js +1 -1
- package/lib/dataflow/tag-utils.js +95 -2
- package/lib/dataflow/tracker.js +6 -6
- package/lib/index.js +2 -0
- package/lib/response-scanning/handlers/utils.js +2 -2
- package/lib/session-configuration/index.js +34 -0
- package/lib/session-configuration/install/http.js +79 -0
- package/package.json +2 -2
|
@@ -32,7 +32,7 @@ const { createSubsetTags } = require('../../../tag-utils');
|
|
|
32
32
|
|
|
33
33
|
const ruleId = 'unvalidated-redirect';
|
|
34
34
|
|
|
35
|
-
module.exports = function
|
|
35
|
+
module.exports = function(core) {
|
|
36
36
|
const {
|
|
37
37
|
depHooks,
|
|
38
38
|
patcher,
|
|
@@ -59,7 +59,7 @@ module.exports = function (core) {
|
|
|
59
59
|
URL_ENCODED,
|
|
60
60
|
];
|
|
61
61
|
|
|
62
|
-
unvalidatedRedirect.install = function
|
|
62
|
+
unvalidatedRedirect.install = function() {
|
|
63
63
|
depHooks.resolve({ name: 'express', file: 'lib/response' }, (Response) => {
|
|
64
64
|
const name = 'Express.Response.location';
|
|
65
65
|
patcher.patch(Response, 'location', {
|
|
@@ -81,7 +81,7 @@ module.exports = function (core) {
|
|
|
81
81
|
|
|
82
82
|
let urlPathTags = strInfo.tags;
|
|
83
83
|
if (url.indexOf('?') > -1) {
|
|
84
|
-
urlPathTags = createSubsetTags(strInfo.tags, 0, url.indexOf('?'));
|
|
84
|
+
urlPathTags = createSubsetTags(strInfo.tags, 0, url.indexOf('?') + 1);
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
if (urlPathTags && isVulnerable(UNTRUSTED, safeTags, urlPathTags)) {
|
|
@@ -92,7 +92,9 @@ module.exports = function (core) {
|
|
|
92
92
|
}],
|
|
93
93
|
context: `response.location(${inspect(strInfo.value)})`,
|
|
94
94
|
history: [strInfo],
|
|
95
|
-
name
|
|
95
|
+
name,
|
|
96
|
+
moduleName: 'express',
|
|
97
|
+
methodName: 'Response.location',
|
|
96
98
|
object: {
|
|
97
99
|
tracked: false,
|
|
98
100
|
value: 'Express.Response',
|
|
@@ -105,6 +107,7 @@ module.exports = function (core) {
|
|
|
105
107
|
source: 'P0',
|
|
106
108
|
stacktraceOpts: {
|
|
107
109
|
constructorOpt: data.hooked,
|
|
110
|
+
prependFrames: [data.orig]
|
|
108
111
|
},
|
|
109
112
|
});
|
|
110
113
|
|
|
@@ -31,6 +31,7 @@ const { createSubsetTags } = require('../../../tag-utils');
|
|
|
31
31
|
const { filterSafeTags, patchType } = require('../../common');
|
|
32
32
|
|
|
33
33
|
const ruleId = 'unvalidated-redirect';
|
|
34
|
+
|
|
34
35
|
const getURLArgument = (args) => {
|
|
35
36
|
if (!Array.isArray(args)) {
|
|
36
37
|
return { index: null, url: undefined };
|
|
@@ -50,7 +51,7 @@ const getURLArgument = (args) => {
|
|
|
50
51
|
};
|
|
51
52
|
};
|
|
52
53
|
|
|
53
|
-
module.exports = function
|
|
54
|
+
module.exports = function(core) {
|
|
54
55
|
const {
|
|
55
56
|
config,
|
|
56
57
|
depHooks,
|
|
@@ -77,9 +78,9 @@ module.exports = function (core) {
|
|
|
77
78
|
URL_ENCODED,
|
|
78
79
|
];
|
|
79
80
|
|
|
80
|
-
unvalidatedRedirect.install = function
|
|
81
|
+
unvalidatedRedirect.install = function() {
|
|
81
82
|
const name = 'fastify.Reply.prototype.redirect';
|
|
82
|
-
depHooks.resolve({ name: 'fastify', file: 'lib/reply' }, (Reply
|
|
83
|
+
depHooks.resolve({ name: 'fastify', file: 'lib/reply' }, (Reply) => {
|
|
83
84
|
patcher.patch(Reply.prototype, 'redirect', {
|
|
84
85
|
name,
|
|
85
86
|
patchType,
|
|
@@ -98,7 +99,7 @@ module.exports = function (core) {
|
|
|
98
99
|
let urlPathTags = strInfo.tags;
|
|
99
100
|
const urlPathEndIdx = url.indexOf('?');
|
|
100
101
|
if (urlPathEndIdx > -1) {
|
|
101
|
-
urlPathTags = createSubsetTags(strInfo.tags, 0, urlPathEndIdx);
|
|
102
|
+
urlPathTags = createSubsetTags(strInfo.tags, 0, urlPathEndIdx + 1);
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
if (isVulnerable(UNTRUSTED, safeTags, urlPathTags)) {
|
|
@@ -119,7 +120,9 @@ module.exports = function (core) {
|
|
|
119
120
|
args,
|
|
120
121
|
context: `reply.redirect(${inspect(strInfo.value)})`,
|
|
121
122
|
history: [strInfo],
|
|
122
|
-
name
|
|
123
|
+
name,
|
|
124
|
+
moduleName: 'fastify',
|
|
125
|
+
methodName: 'reply.redirect',
|
|
123
126
|
object: {
|
|
124
127
|
tracked: false,
|
|
125
128
|
value: 'fastify.Reply',
|
|
@@ -132,6 +135,7 @@ module.exports = function (core) {
|
|
|
132
135
|
source: `P${valueIndex}`,
|
|
133
136
|
stacktraceOpts: {
|
|
134
137
|
constructorOpt: data.hooked,
|
|
138
|
+
prependFrames: [data.orig]
|
|
135
139
|
},
|
|
136
140
|
});
|
|
137
141
|
|
|
@@ -15,9 +15,22 @@
|
|
|
15
15
|
|
|
16
16
|
'use strict';
|
|
17
17
|
const { patchType } = require('../common');
|
|
18
|
-
const {
|
|
18
|
+
const {
|
|
19
|
+
FS_METHODS,
|
|
20
|
+
Rule,
|
|
21
|
+
isString,
|
|
22
|
+
DataflowTag: {
|
|
23
|
+
URL_ENCODED,
|
|
24
|
+
LIMITED_CHARS,
|
|
25
|
+
ALPHANUM_SPACE_HYPHEN,
|
|
26
|
+
SAFE_PATH,
|
|
27
|
+
UNTRUSTED,
|
|
28
|
+
},
|
|
29
|
+
inspect,
|
|
30
|
+
join,
|
|
31
|
+
} = require('@contrast/common');
|
|
19
32
|
|
|
20
|
-
module.exports = function
|
|
33
|
+
module.exports = function(core) {
|
|
21
34
|
const {
|
|
22
35
|
depHooks,
|
|
23
36
|
patcher,
|
|
@@ -31,7 +44,12 @@ module.exports = function (core) {
|
|
|
31
44
|
},
|
|
32
45
|
} = core;
|
|
33
46
|
|
|
34
|
-
const safeTags = [
|
|
47
|
+
const safeTags = [
|
|
48
|
+
URL_ENCODED,
|
|
49
|
+
LIMITED_CHARS,
|
|
50
|
+
ALPHANUM_SPACE_HYPHEN,
|
|
51
|
+
SAFE_PATH,
|
|
52
|
+
];
|
|
35
53
|
|
|
36
54
|
function getValues(indices, args) {
|
|
37
55
|
return indices.reduce((acc, idx) => {
|
|
@@ -41,33 +59,47 @@ module.exports = function (core) {
|
|
|
41
59
|
}, []);
|
|
42
60
|
}
|
|
43
61
|
|
|
44
|
-
const pre = (name,
|
|
62
|
+
const pre = (name, method, moduleName = 'fs', fullMethodName = '') => (data) => {
|
|
63
|
+
const { name: methodName, indices } = method;
|
|
45
64
|
const store = sources.getStore()?.assess;
|
|
46
65
|
if (!store) return;
|
|
47
66
|
|
|
48
67
|
const values = getValues(indices, data.args);
|
|
49
68
|
if (!values.length) return;
|
|
50
69
|
|
|
51
|
-
const args =
|
|
70
|
+
const args = values.map((v) => {
|
|
71
|
+
const strInfo = tracker.getData(v);
|
|
72
|
+
return {
|
|
73
|
+
value: strInfo ? strInfo.value : v,
|
|
74
|
+
tracked: !!strInfo,
|
|
75
|
+
strInfo
|
|
76
|
+
};
|
|
77
|
+
});
|
|
52
78
|
for (let i = 0; i < values.length; i++) {
|
|
53
|
-
const strInfo =
|
|
54
|
-
args.push({ value: strInfo ? strInfo.value : values[i], isTracked: !!strInfo });
|
|
79
|
+
const { strInfo } = args[i];
|
|
55
80
|
if (!strInfo || !isVulnerable(UNTRUSTED, safeTags, strInfo.tags)) {
|
|
56
81
|
continue;
|
|
57
82
|
}
|
|
58
83
|
|
|
59
84
|
const event = createSinkEvent({
|
|
60
85
|
name,
|
|
86
|
+
moduleName,
|
|
87
|
+
methodName: fullMethodName || methodName,
|
|
88
|
+
context: `${name}(${join(
|
|
89
|
+
args.map((a) => inspect(a.value)),
|
|
90
|
+
', '
|
|
91
|
+
)})`,
|
|
61
92
|
history: [strInfo],
|
|
62
93
|
object: {
|
|
63
94
|
value: 'fs',
|
|
64
|
-
|
|
95
|
+
tracked: false,
|
|
65
96
|
},
|
|
66
|
-
args,
|
|
97
|
+
args: args.map(({ value, tracked }) => ({ value, tracked })),
|
|
67
98
|
tags: strInfo.tags,
|
|
68
99
|
source: `P${i}`,
|
|
69
100
|
stacktraceOpts: {
|
|
70
101
|
contructorOpt: data.hooked,
|
|
102
|
+
prependFrames: [data.orig],
|
|
71
103
|
},
|
|
72
104
|
});
|
|
73
105
|
|
|
@@ -90,7 +122,7 @@ module.exports = function (core) {
|
|
|
90
122
|
patcher.patch(fs, method.name, {
|
|
91
123
|
name,
|
|
92
124
|
patchType,
|
|
93
|
-
pre: pre(name, method
|
|
125
|
+
pre: pre(name, method),
|
|
94
126
|
});
|
|
95
127
|
}
|
|
96
128
|
|
|
@@ -101,7 +133,7 @@ module.exports = function (core) {
|
|
|
101
133
|
patcher.patch(fs, syncName, {
|
|
102
134
|
name,
|
|
103
135
|
patchType,
|
|
104
|
-
pre: pre(name, method
|
|
136
|
+
pre: pre(name, method, 'fs', syncName),
|
|
105
137
|
});
|
|
106
138
|
}
|
|
107
139
|
}
|
|
@@ -111,7 +143,7 @@ module.exports = function (core) {
|
|
|
111
143
|
patcher.patch(fs.promises, method.name, {
|
|
112
144
|
name,
|
|
113
145
|
patchType,
|
|
114
|
-
pre: pre(name, method.
|
|
146
|
+
pre: pre(name, method, 'fs.promises'),
|
|
115
147
|
});
|
|
116
148
|
}
|
|
117
149
|
}
|
|
@@ -124,7 +156,7 @@ module.exports = function (core) {
|
|
|
124
156
|
patcher.patch(fsPromises, method.name, {
|
|
125
157
|
name,
|
|
126
158
|
patchType,
|
|
127
|
-
pre: pre(name, method
|
|
159
|
+
pre: pre(name, method, 'fsPromises'),
|
|
128
160
|
});
|
|
129
161
|
}
|
|
130
162
|
}
|
|
@@ -0,0 +1,160 @@
|
|
|
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 {
|
|
19
|
+
isString,
|
|
20
|
+
inspect,
|
|
21
|
+
join,
|
|
22
|
+
DataflowTag: {
|
|
23
|
+
UNTRUSTED,
|
|
24
|
+
CUSTOM_ENCODED_TRUST_BOUNDARY_VIOLATION,
|
|
25
|
+
CUSTOM_ENCODED,
|
|
26
|
+
CUSTOM_VALIDATED_TRUST_BOUNDARY_VIOLATION,
|
|
27
|
+
CUSTOM_VALIDATED,
|
|
28
|
+
LIMITED_CHARS,
|
|
29
|
+
},
|
|
30
|
+
} = require('@contrast/common');
|
|
31
|
+
const { patchType, filterSafeTags } = require('../common');
|
|
32
|
+
|
|
33
|
+
const ruleId = 'unsafe-code-execution';
|
|
34
|
+
const safeTags = [
|
|
35
|
+
CUSTOM_ENCODED_TRUST_BOUNDARY_VIOLATION,
|
|
36
|
+
CUSTOM_ENCODED,
|
|
37
|
+
CUSTOM_VALIDATED_TRUST_BOUNDARY_VIOLATION,
|
|
38
|
+
CUSTOM_VALIDATED,
|
|
39
|
+
LIMITED_CHARS,
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
module.exports = function(core) {
|
|
43
|
+
const {
|
|
44
|
+
config,
|
|
45
|
+
logger,
|
|
46
|
+
patcher,
|
|
47
|
+
scopes: { sources, instrumentation },
|
|
48
|
+
assess: {
|
|
49
|
+
dataflow: {
|
|
50
|
+
tracker,
|
|
51
|
+
sinks: { isVulnerable, reportFindings, reportSafePositive },
|
|
52
|
+
eventFactory: { createSinkEvent },
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
} = core;
|
|
56
|
+
|
|
57
|
+
core.assess.dataflow.sinks.contrastFunction = {
|
|
58
|
+
install() {
|
|
59
|
+
if (!global.ContrastMethods?.Function) {
|
|
60
|
+
logger.error(
|
|
61
|
+
'Cannot install `Function` instrumentation - Contrast method DNE'
|
|
62
|
+
);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
Object.assign(global.ContrastMethods, {
|
|
67
|
+
Function: patcher.patch(global.ContrastMethods.Function, {
|
|
68
|
+
name: 'global.ContrastMethods.Function',
|
|
69
|
+
patchType,
|
|
70
|
+
pre({ args: origArgs, hooked, orig, name }) {
|
|
71
|
+
const store = sources.getStore()?.assess;
|
|
72
|
+
const fnBody = origArgs[origArgs.length - 1];
|
|
73
|
+
if (
|
|
74
|
+
!store ||
|
|
75
|
+
instrumentation.isLocked() ||
|
|
76
|
+
!fnBody ||
|
|
77
|
+
!isString(fnBody)
|
|
78
|
+
)
|
|
79
|
+
return;
|
|
80
|
+
|
|
81
|
+
const strInfo = tracker.getData(fnBody);
|
|
82
|
+
|
|
83
|
+
if (!strInfo) return;
|
|
84
|
+
|
|
85
|
+
const isArgVulnerable = isVulnerable(UNTRUSTED, safeTags, strInfo.tags);
|
|
86
|
+
|
|
87
|
+
if (!isArgVulnerable && config.assess.safe_positives.enable) {
|
|
88
|
+
const foundSafeTags = filterSafeTags(safeTags, strInfo);
|
|
89
|
+
const safeStrInfo = {
|
|
90
|
+
value: strInfo.value,
|
|
91
|
+
tags: strInfo.tags,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
reportSafePositive({
|
|
95
|
+
name,
|
|
96
|
+
ruleId,
|
|
97
|
+
safeTags: foundSafeTags,
|
|
98
|
+
strInfo: safeStrInfo,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (isArgVulnerable) {
|
|
103
|
+
const args = origArgs.map((arg, idx) => {
|
|
104
|
+
if (idx === origArgs.length - 1) {
|
|
105
|
+
return {
|
|
106
|
+
value: strInfo.value,
|
|
107
|
+
tracked: true,
|
|
108
|
+
inspectedValue: `'${strInfo.value}'`,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const inspectedValue = inspect(arg);
|
|
113
|
+
const argInfo = arg && isString(arg) ? tracker.getData(arg) : null;
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
value: argInfo ? argInfo.value : inspectedValue,
|
|
117
|
+
tracked: !!argInfo,
|
|
118
|
+
inspectedValue
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
const event = createSinkEvent({
|
|
122
|
+
name,
|
|
123
|
+
context: `${name}(${join(
|
|
124
|
+
args.map((a) => a.inspectedValue),
|
|
125
|
+
', '
|
|
126
|
+
)})`,
|
|
127
|
+
history: [strInfo],
|
|
128
|
+
object: {
|
|
129
|
+
value: 'global.ContrastMethods',
|
|
130
|
+
tracked: false,
|
|
131
|
+
},
|
|
132
|
+
moduleName: 'global.ContrastMethods',
|
|
133
|
+
methodName: 'Function',
|
|
134
|
+
args: args.map((a) => ({
|
|
135
|
+
value: a.value,
|
|
136
|
+
tracked: a.tracked,
|
|
137
|
+
})),
|
|
138
|
+
tags: strInfo.tags,
|
|
139
|
+
source: `P${origArgs.length - 1}`,
|
|
140
|
+
stacktraceOpts: {
|
|
141
|
+
contructorOpt: hooked,
|
|
142
|
+
prependFrames: [orig],
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
if (event) {
|
|
147
|
+
reportFindings({
|
|
148
|
+
ruleId,
|
|
149
|
+
sinkEvent: event,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
}),
|
|
155
|
+
});
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
return core.assess.dataflow.sinks.contrastFunction;
|
|
160
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
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 { callChildComponentMethodsSync } = require('@contrast/common');
|
|
19
|
+
|
|
20
|
+
module.exports = function(core) {
|
|
21
|
+
const http = core.assess.dataflow.sinks.http = {};
|
|
22
|
+
|
|
23
|
+
require('./request')(core);
|
|
24
|
+
require('./server-response')(core);
|
|
25
|
+
|
|
26
|
+
http.install = function() {
|
|
27
|
+
callChildComponentMethodsSync(http, 'install');
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return http;
|
|
31
|
+
};
|
|
@@ -0,0 +1,152 @@
|
|
|
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 {
|
|
19
|
+
inspect,
|
|
20
|
+
isString,
|
|
21
|
+
DataflowTag: {
|
|
22
|
+
UNTRUSTED,
|
|
23
|
+
CUSTOM_ENCODED_SSRF,
|
|
24
|
+
CUSTOM_ENCODED,
|
|
25
|
+
CUSTOM_VALIDATED_SSRF,
|
|
26
|
+
CUSTOM_VALIDATED,
|
|
27
|
+
LIMITED_CHARS
|
|
28
|
+
}
|
|
29
|
+
} = require('@contrast/common');
|
|
30
|
+
const Url = require('url');
|
|
31
|
+
const trustedLibs = [/^(?!.*(newrelic)).*http.*$/];
|
|
32
|
+
const { patchType } = require('../../common');
|
|
33
|
+
const { createAppendTags } = require('../../../tag-utils');
|
|
34
|
+
|
|
35
|
+
module.exports = function(core) {
|
|
36
|
+
const {
|
|
37
|
+
depHooks,
|
|
38
|
+
patcher,
|
|
39
|
+
scopes: { sources },
|
|
40
|
+
assess: {
|
|
41
|
+
dataflow: {
|
|
42
|
+
tracker,
|
|
43
|
+
sinks: {
|
|
44
|
+
isVulnerable,
|
|
45
|
+
reportFindings
|
|
46
|
+
},
|
|
47
|
+
eventFactory: { createSinkEvent },
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
} = core;
|
|
51
|
+
const http = core.assess.dataflow.sinks.http.request = {};
|
|
52
|
+
|
|
53
|
+
const safeTags = [
|
|
54
|
+
CUSTOM_ENCODED_SSRF,
|
|
55
|
+
CUSTOM_ENCODED,
|
|
56
|
+
CUSTOM_VALIDATED_SSRF,
|
|
57
|
+
CUSTOM_VALIDATED,
|
|
58
|
+
LIMITED_CHARS
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
function getValueFromReq(value, key) {
|
|
62
|
+
if (isString(value)) {
|
|
63
|
+
const url = new Url.URL(value);
|
|
64
|
+
if (isString(url[key])) {
|
|
65
|
+
return url[key];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (isString(value[key])) {
|
|
70
|
+
return value[key];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function containsTrustedLib(stack) {
|
|
75
|
+
for (const { file } of stack) {
|
|
76
|
+
for (const trusted of trustedLibs) {
|
|
77
|
+
if (trusted.exec(file)) {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
http.install = function() {
|
|
87
|
+
['http', 'https'].forEach((moduleName) => {
|
|
88
|
+
depHooks.resolve({ name: moduleName }, (module) => {
|
|
89
|
+
const name = `${moduleName}.request`;
|
|
90
|
+
const methodName = 'request';
|
|
91
|
+
patcher.patch(module, methodName, {
|
|
92
|
+
name,
|
|
93
|
+
patchType,
|
|
94
|
+
pre(data) {
|
|
95
|
+
const sourceContext = sources.getStore()?.assess;
|
|
96
|
+
if (!sourceContext) return;
|
|
97
|
+
|
|
98
|
+
const [req] = data.args;
|
|
99
|
+
if (!req) return;
|
|
100
|
+
|
|
101
|
+
['host', 'hostname', 'localAddress', 'protocol'].forEach((key) => {
|
|
102
|
+
const value = getValueFromReq(req, key);
|
|
103
|
+
if (!value) return;
|
|
104
|
+
|
|
105
|
+
const strInfo = tracker.getData(value);
|
|
106
|
+
if (!strInfo) return;
|
|
107
|
+
|
|
108
|
+
if (containsTrustedLib(strInfo.stack)) return;
|
|
109
|
+
|
|
110
|
+
const arg0 = isString(req) ? req : inspect(req);
|
|
111
|
+
const idx = arg0.indexOf(value);
|
|
112
|
+
const urlTags = createAppendTags({}, strInfo.tags, idx);
|
|
113
|
+
|
|
114
|
+
if (urlTags && isVulnerable(UNTRUSTED, safeTags, urlTags)) {
|
|
115
|
+
const event = createSinkEvent({
|
|
116
|
+
name,
|
|
117
|
+
moduleName,
|
|
118
|
+
methodName: 'request',
|
|
119
|
+
context: `${moduleName}.request(${arg0})`,
|
|
120
|
+
history: [strInfo],
|
|
121
|
+
object: {
|
|
122
|
+
tracked: false,
|
|
123
|
+
value: moduleName
|
|
124
|
+
},
|
|
125
|
+
args: [{
|
|
126
|
+
value: arg0,
|
|
127
|
+
tracked: true
|
|
128
|
+
}],
|
|
129
|
+
source: 'P0',
|
|
130
|
+
stacktraceOpts: {
|
|
131
|
+
constructorOpt: data.hooked,
|
|
132
|
+
prependFrames: [data.orig]
|
|
133
|
+
},
|
|
134
|
+
tags: urlTags,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
if (event) {
|
|
138
|
+
reportFindings({
|
|
139
|
+
ruleId: 'ssrf',
|
|
140
|
+
sinkEvent: event
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
return http;
|
|
152
|
+
};
|
|
@@ -31,7 +31,7 @@ const {
|
|
|
31
31
|
},
|
|
32
32
|
Rule: { REFLECTED_XSS: ruleId },
|
|
33
33
|
} = require('@contrast/common');
|
|
34
|
-
const { patchType, filterSafeTags } = require('
|
|
34
|
+
const { patchType, filterSafeTags } = require('../../common');
|
|
35
35
|
|
|
36
36
|
module.exports = function(core) {
|
|
37
37
|
const {
|
|
@@ -52,7 +52,7 @@ module.exports = function(core) {
|
|
|
52
52
|
},
|
|
53
53
|
},
|
|
54
54
|
} = core;
|
|
55
|
-
const http = core.assess.dataflow.sinks.http = {};
|
|
55
|
+
const http = core.assess.dataflow.sinks.http.serverResponse = {};
|
|
56
56
|
|
|
57
57
|
const safeTags = [
|
|
58
58
|
ALPHANUM_SPACE_HYPHEN,
|
|
@@ -86,9 +86,11 @@ module.exports = function(core) {
|
|
|
86
86
|
value: strInfo.value,
|
|
87
87
|
tracked: true
|
|
88
88
|
}],
|
|
89
|
-
context: `res.${method}(${strInfo.value})`,
|
|
89
|
+
context: `res.${method}('${strInfo.value}')`,
|
|
90
90
|
history: [strInfo],
|
|
91
91
|
name,
|
|
92
|
+
moduleName: 'http',
|
|
93
|
+
methodName: `ServerResponse.prototype.${method}`,
|
|
92
94
|
object: {
|
|
93
95
|
tracked: false,
|
|
94
96
|
value: 'http.ServerResponse'
|
|
@@ -99,7 +101,8 @@ module.exports = function(core) {
|
|
|
99
101
|
},
|
|
100
102
|
source: 'P0',
|
|
101
103
|
stacktraceOpts: {
|
|
102
|
-
constructorOpt: data.hooked
|
|
104
|
+
constructorOpt: data.hooked,
|
|
105
|
+
prependFrames: [data.orig]
|
|
103
106
|
},
|
|
104
107
|
tags: strInfo.tags,
|
|
105
108
|
});
|
|
@@ -32,7 +32,7 @@ const { filterSafeTags, patchType } = require('../../common');
|
|
|
32
32
|
|
|
33
33
|
const ruleId = 'unvalidated-redirect';
|
|
34
34
|
|
|
35
|
-
module.exports = function
|
|
35
|
+
module.exports = function(core) {
|
|
36
36
|
const {
|
|
37
37
|
depHooks,
|
|
38
38
|
patcher,
|
|
@@ -59,11 +59,11 @@ module.exports = function (core) {
|
|
|
59
59
|
URL_ENCODED,
|
|
60
60
|
];
|
|
61
61
|
|
|
62
|
-
unvalidatedRedirect.install = function
|
|
62
|
+
unvalidatedRedirect.install = function() {
|
|
63
63
|
depHooks.resolve({ name: 'koa', file: 'lib/response', version: '<2.9.0' }, (Response) => {
|
|
64
64
|
const name = 'Koa.Response.redirect';
|
|
65
65
|
patcher.patch(Response, 'redirect', {
|
|
66
|
-
name
|
|
66
|
+
name,
|
|
67
67
|
patchType,
|
|
68
68
|
pre(data) {
|
|
69
69
|
const assessStore = sources.getStore()?.assess;
|
|
@@ -84,7 +84,7 @@ module.exports = function (core) {
|
|
|
84
84
|
let urlPathTags = strInfo.tags;
|
|
85
85
|
|
|
86
86
|
if (url.indexOf('?') > -1) {
|
|
87
|
-
urlPathTags = createSubsetTags(strInfo.tags, 0, url.indexOf('?'));
|
|
87
|
+
urlPathTags = createSubsetTags(strInfo.tags, 0, url.indexOf('?') + 1);
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
if (urlPathTags && isVulnerable(UNTRUSTED, safeTags, urlPathTags)) {
|
|
@@ -104,7 +104,9 @@ module.exports = function (core) {
|
|
|
104
104
|
args,
|
|
105
105
|
context: `response.redirect(${inspect(strInfo.value)})`,
|
|
106
106
|
history: [strInfo],
|
|
107
|
-
name
|
|
107
|
+
name,
|
|
108
|
+
moduleName: 'koa',
|
|
109
|
+
methodName: 'Response.redirect',
|
|
108
110
|
object: {
|
|
109
111
|
tracked: false,
|
|
110
112
|
value: 'Koa.Response',
|
|
@@ -117,6 +119,7 @@ module.exports = function (core) {
|
|
|
117
119
|
source: `P${isBackRoute ? 1 : 0}`,
|
|
118
120
|
stacktraceOpts: {
|
|
119
121
|
constructorOpt: data.hooked,
|
|
122
|
+
prependFrames: [data.orig]
|
|
120
123
|
},
|
|
121
124
|
});
|
|
122
125
|
|
|
@@ -102,6 +102,8 @@ module.exports = function(core) {
|
|
|
102
102
|
const sinkEvent = createSinkEvent({
|
|
103
103
|
args,
|
|
104
104
|
context: `marsdb.Collection.${method}(${args.map((a) => a.value)})`,
|
|
105
|
+
moduleName: 'marsdb',
|
|
106
|
+
methodName: `Collection.prototype.${method}`,
|
|
105
107
|
history: [strInfo],
|
|
106
108
|
object: {
|
|
107
109
|
tracked: false,
|
|
@@ -112,6 +114,7 @@ module.exports = function(core) {
|
|
|
112
114
|
source: `P${argIdx}`,
|
|
113
115
|
stacktraceOpts: {
|
|
114
116
|
constructorOpt: data.hooked,
|
|
117
|
+
prependFrames: [data.orig]
|
|
115
118
|
},
|
|
116
119
|
tags: strInfo.tags,
|
|
117
120
|
});
|