@contrast/assess 1.5.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/dataflow/event-factory.js +10 -5
- package/lib/dataflow/propagation/install/decode-uri-component.js +5 -2
- package/lib/dataflow/propagation/install/ejs/escape-xml.js +5 -2
- package/lib/dataflow/propagation/install/escape-html.js +7 -4
- package/lib/dataflow/propagation/install/escape.js +5 -2
- package/lib/dataflow/propagation/install/handlebars-utils-escape-expression.js +5 -2
- package/lib/dataflow/propagation/install/mysql-connection-escape.js +5 -2
- package/lib/dataflow/propagation/install/querystring/parse.js +8 -3
- package/lib/dataflow/propagation/install/string/replace.js +5 -1
- package/lib/dataflow/propagation/install/unescape.js +5 -2
- package/lib/dataflow/propagation/install/validator/methods.js +60 -51
- package/lib/dataflow/sinks/index.js +4 -0
- package/lib/dataflow/sinks/install/child-process.js +150 -13
- package/lib/dataflow/sinks/install/fastify/unvalidated-redirect.js +19 -8
- package/lib/dataflow/sinks/install/fs.js +136 -0
- package/lib/dataflow/sinks/install/http.js +27 -13
- package/lib/dataflow/sinks/install/koa/unvalidated-redirect.js +17 -8
- package/lib/dataflow/sinks/install/marsdb.js +135 -0
- package/lib/dataflow/sinks/install/mongodb.js +205 -0
- package/lib/dataflow/sinks/install/mssql.js +11 -7
- package/lib/dataflow/sinks/install/mysql.js +122 -0
- package/lib/dataflow/sinks/install/postgres.js +13 -12
- package/lib/dataflow/sinks/install/sqlite3.js +12 -7
- package/lib/dataflow/sources/handler.js +10 -9
- package/package.json +2 -2
|
@@ -16,7 +16,17 @@
|
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
18
|
const util = require('util');
|
|
19
|
-
const {
|
|
19
|
+
const {
|
|
20
|
+
DataflowTag: {
|
|
21
|
+
UNTRUSTED,
|
|
22
|
+
CUSTOM_ENCODED,
|
|
23
|
+
CUSTOM_VALIDATED,
|
|
24
|
+
HTML_ENCODED,
|
|
25
|
+
LIMITED_CHARS,
|
|
26
|
+
URL_ENCODED,
|
|
27
|
+
},
|
|
28
|
+
isString
|
|
29
|
+
} = require('@contrast/common');
|
|
20
30
|
const { patchType } = require('../../common');
|
|
21
31
|
const { createSubsetTags } = require('../../../tag-utils');
|
|
22
32
|
|
|
@@ -39,13 +49,12 @@ module.exports = function (core) {
|
|
|
39
49
|
const inspect = patcher.unwrap(util.inspect);
|
|
40
50
|
|
|
41
51
|
const safeTags = [
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
52
|
+
CUSTOM_ENCODED,
|
|
53
|
+
CUSTOM_VALIDATED,
|
|
54
|
+
HTML_ENCODED,
|
|
55
|
+
LIMITED_CHARS,
|
|
56
|
+
URL_ENCODED,
|
|
47
57
|
];
|
|
48
|
-
const requiredTag = 'untrusted';
|
|
49
58
|
|
|
50
59
|
/**
|
|
51
60
|
* Patches `Reply.prototype.redirect` for
|
|
@@ -70,6 +79,8 @@ module.exports = function (core) {
|
|
|
70
79
|
const strInfo = tracker.getData(url);
|
|
71
80
|
if (!strInfo) return;
|
|
72
81
|
|
|
82
|
+
// todo: how does different tag logic play into display ranges?
|
|
83
|
+
|
|
73
84
|
let urlPathTags = strInfo.tags;
|
|
74
85
|
const urlPathEndIdx = url.indexOf('?');
|
|
75
86
|
|
|
@@ -77,7 +88,7 @@ module.exports = function (core) {
|
|
|
77
88
|
urlPathTags = createSubsetTags(strInfo.tags, 0, urlPathEndIdx);
|
|
78
89
|
}
|
|
79
90
|
|
|
80
|
-
if (isVulnerable(
|
|
91
|
+
if (isVulnerable(UNTRUSTED, safeTags, urlPathTags)) {
|
|
81
92
|
const event = createSinkEvent({
|
|
82
93
|
args: [{
|
|
83
94
|
tracked: true,
|
|
@@ -0,0 +1,136 @@
|
|
|
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
|
+
const { patchType } = require('../common');
|
|
18
|
+
const { FS_METHODS, Rule, isString, DataflowTag: { URL_ENCODED, LIMITED_CHARS, ALPHANUM_SPACE_HYPHEN, SAFE_PATH, UNTRUSTED } } = require('@contrast/common');
|
|
19
|
+
|
|
20
|
+
module.exports = function (core) {
|
|
21
|
+
const {
|
|
22
|
+
depHooks,
|
|
23
|
+
patcher,
|
|
24
|
+
scopes: { sources },
|
|
25
|
+
assess: {
|
|
26
|
+
dataflow: {
|
|
27
|
+
tracker,
|
|
28
|
+
sinks: { isVulnerable, reportFindings },
|
|
29
|
+
eventFactory: { createSinkEvent },
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
} = core;
|
|
33
|
+
|
|
34
|
+
const safeTags = [URL_ENCODED, LIMITED_CHARS, ALPHANUM_SPACE_HYPHEN, SAFE_PATH];
|
|
35
|
+
|
|
36
|
+
function getValues(indices, args) {
|
|
37
|
+
return indices.reduce((acc, idx) => {
|
|
38
|
+
const value = args[idx];
|
|
39
|
+
if (value && isString(value)) acc.push(value);
|
|
40
|
+
return acc;
|
|
41
|
+
}, []);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const pre = (name, indices) => (data) => {
|
|
45
|
+
const store = sources.getStore()?.assess;
|
|
46
|
+
if (!store) return;
|
|
47
|
+
|
|
48
|
+
const values = getValues(indices, data.args);
|
|
49
|
+
if (!values.length) return;
|
|
50
|
+
|
|
51
|
+
const args = [];
|
|
52
|
+
for (let i = 0; i < values.length; i++) {
|
|
53
|
+
const strInfo = tracker.getData(values[i]);
|
|
54
|
+
args.push({ value: strInfo ? strInfo.value : values[i], isTracked: !!strInfo });
|
|
55
|
+
if (!strInfo || !isVulnerable(UNTRUSTED, safeTags, strInfo.tags)) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const event = createSinkEvent({
|
|
60
|
+
name,
|
|
61
|
+
history: [strInfo],
|
|
62
|
+
object: {
|
|
63
|
+
value: 'fs',
|
|
64
|
+
isTracked: false,
|
|
65
|
+
},
|
|
66
|
+
args,
|
|
67
|
+
tags: strInfo.tags,
|
|
68
|
+
source: `P${i}`,
|
|
69
|
+
stacktraceOpts: {
|
|
70
|
+
contructorOpt: data.hooked,
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (event) {
|
|
75
|
+
reportFindings({
|
|
76
|
+
ruleId: Rule.PATH_TRAVERSAL,
|
|
77
|
+
sinkEvent: event,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
core.assess.dataflow.sinks.pathTraversal = {
|
|
84
|
+
install() {
|
|
85
|
+
depHooks.resolve({ name: 'fs' }, (fs) => {
|
|
86
|
+
for (const method of FS_METHODS) {
|
|
87
|
+
// not all methods are available on every OS or Node version.
|
|
88
|
+
if (fs[method.name]) {
|
|
89
|
+
const name = `fs.${method.name}`;
|
|
90
|
+
patcher.patch(fs, method.name, {
|
|
91
|
+
name,
|
|
92
|
+
patchType,
|
|
93
|
+
pre: pre(name, method.indices)
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (method.sync) {
|
|
98
|
+
const syncName = `${method.name}Sync`;
|
|
99
|
+
if (fs[syncName]) {
|
|
100
|
+
const name = `fs.${syncName}`;
|
|
101
|
+
patcher.patch(fs, syncName, {
|
|
102
|
+
name,
|
|
103
|
+
patchType,
|
|
104
|
+
pre: pre(name, method.indices)
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (method.promises && fs.promises && fs.promises[method.name]) {
|
|
110
|
+
const name = `fs.promises.${method.name}`;
|
|
111
|
+
patcher.patch(fs.promises, method.name, {
|
|
112
|
+
name,
|
|
113
|
+
patchType,
|
|
114
|
+
pre: pre(name, method.indices)
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
depHooks.resolve({ name: 'fs/promises' }, (fsPromises) => {
|
|
121
|
+
for (const method of FS_METHODS) {
|
|
122
|
+
if (method.promises && fsPromises[method.name]) {
|
|
123
|
+
const name = `fsPromises.${method.name}`;
|
|
124
|
+
patcher.patch(fsPromises, method.name, {
|
|
125
|
+
name,
|
|
126
|
+
patchType,
|
|
127
|
+
pre: pre(name, method.indices)
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
return core.assess.dataflow.sinks.pathTraversal;
|
|
136
|
+
};
|
|
@@ -15,7 +15,22 @@
|
|
|
15
15
|
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
|
-
const {
|
|
18
|
+
const {
|
|
19
|
+
DataflowTag: {
|
|
20
|
+
UNTRUSTED,
|
|
21
|
+
ALPHANUM_SPACE_HYPHEN,
|
|
22
|
+
COOKIE,
|
|
23
|
+
CUSTOM_ENCODED,
|
|
24
|
+
CUSTOM_VALIDATED,
|
|
25
|
+
HEADER,
|
|
26
|
+
HTML_ENCODED,
|
|
27
|
+
LIMITED_CHARS,
|
|
28
|
+
SQL_ENCODED,
|
|
29
|
+
URL_ENCODED,
|
|
30
|
+
WEAK_URL_ENCODED,
|
|
31
|
+
},
|
|
32
|
+
Rule,
|
|
33
|
+
} = require('@contrast/common');
|
|
19
34
|
const { patchType } = require('../common');
|
|
20
35
|
|
|
21
36
|
module.exports = function(core) {
|
|
@@ -34,18 +49,17 @@ module.exports = function(core) {
|
|
|
34
49
|
const http = core.assess.dataflow.sinks.http = {};
|
|
35
50
|
|
|
36
51
|
const safeTags = [
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
52
|
+
ALPHANUM_SPACE_HYPHEN,
|
|
53
|
+
COOKIE,
|
|
54
|
+
CUSTOM_ENCODED,
|
|
55
|
+
CUSTOM_VALIDATED,
|
|
56
|
+
HEADER,
|
|
57
|
+
HTML_ENCODED,
|
|
58
|
+
LIMITED_CHARS,
|
|
59
|
+
SQL_ENCODED,
|
|
60
|
+
URL_ENCODED,
|
|
61
|
+
WEAK_URL_ENCODED,
|
|
47
62
|
];
|
|
48
|
-
const requiredTag = 'untrusted';
|
|
49
63
|
|
|
50
64
|
const preHook = (name, method) => (data) => {
|
|
51
65
|
const sourceContext = sources.getStore()?.assess;
|
|
@@ -60,7 +74,7 @@ module.exports = function(core) {
|
|
|
60
74
|
const { contentType } = sourceContext.responseData;
|
|
61
75
|
if (contentType && isSafeContentType(contentType)) return;
|
|
62
76
|
|
|
63
|
-
if (isVulnerable(
|
|
77
|
+
if (isVulnerable(UNTRUSTED, safeTags, strInfo.tags)) {
|
|
64
78
|
const event = createSinkEvent({
|
|
65
79
|
args: [{
|
|
66
80
|
value: strInfo.value,
|
|
@@ -16,7 +16,17 @@
|
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
18
|
const util = require('util');
|
|
19
|
-
const {
|
|
19
|
+
const {
|
|
20
|
+
DataflowTag: {
|
|
21
|
+
UNTRUSTED,
|
|
22
|
+
CUSTOM_ENCODED,
|
|
23
|
+
CUSTOM_VALIDATED,
|
|
24
|
+
HTML_ENCODED,
|
|
25
|
+
LIMITED_CHARS,
|
|
26
|
+
URL_ENCODED,
|
|
27
|
+
},
|
|
28
|
+
isString
|
|
29
|
+
} = require('@contrast/common');
|
|
20
30
|
const { patchType } = require('../../common');
|
|
21
31
|
const { createSubsetTags } = require('../../../tag-utils');
|
|
22
32
|
|
|
@@ -39,13 +49,12 @@ module.exports = function (core) {
|
|
|
39
49
|
const inspect = patcher.unwrap(util.inspect);
|
|
40
50
|
|
|
41
51
|
const safeTags = [
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
52
|
+
CUSTOM_ENCODED,
|
|
53
|
+
CUSTOM_VALIDATED,
|
|
54
|
+
HTML_ENCODED,
|
|
55
|
+
LIMITED_CHARS,
|
|
56
|
+
URL_ENCODED,
|
|
47
57
|
];
|
|
48
|
-
const requiredTag = 'untrusted';
|
|
49
58
|
|
|
50
59
|
unvalidatedRedirect.install = function () {
|
|
51
60
|
depHooks.resolve({ name: 'koa', file: 'lib/response', version: '<2.9.0' }, (Response) => {
|
|
@@ -73,7 +82,7 @@ module.exports = function (core) {
|
|
|
73
82
|
urlPathTags = createSubsetTags(strInfo.tags, 0, url.indexOf('?'));
|
|
74
83
|
}
|
|
75
84
|
|
|
76
|
-
if (urlPathTags && isVulnerable(
|
|
85
|
+
if (urlPathTags && isVulnerable(UNTRUSTED, safeTags, urlPathTags)) {
|
|
77
86
|
const event = createSinkEvent({
|
|
78
87
|
args: [{
|
|
79
88
|
tracked: true,
|
|
@@ -0,0 +1,135 @@
|
|
|
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
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const util = require('util');
|
|
18
|
+
const { patchType } = require('../common');
|
|
19
|
+
const {
|
|
20
|
+
traverseValues,
|
|
21
|
+
Rule,
|
|
22
|
+
DataflowTag: {
|
|
23
|
+
ALPHANUM_SPACE_HYPHEN,
|
|
24
|
+
LIMITED_CHARS,
|
|
25
|
+
UNTRUSTED,
|
|
26
|
+
STRING_TYPE_CHECKED,
|
|
27
|
+
CUSTOM_VALIDATED_NOSQL_INJECTION,
|
|
28
|
+
},
|
|
29
|
+
} = require('@contrast/common');
|
|
30
|
+
|
|
31
|
+
const collectionMethods = ['find', 'findOne', 'update', 'remove'];
|
|
32
|
+
const querySafeTags = [
|
|
33
|
+
LIMITED_CHARS,
|
|
34
|
+
ALPHANUM_SPACE_HYPHEN,
|
|
35
|
+
STRING_TYPE_CHECKED,
|
|
36
|
+
CUSTOM_VALIDATED_NOSQL_INJECTION,
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
module.exports = function(core) {
|
|
40
|
+
const {
|
|
41
|
+
depHooks,
|
|
42
|
+
logger,
|
|
43
|
+
patcher,
|
|
44
|
+
scopes: { sources, instrumentation },
|
|
45
|
+
assess: {
|
|
46
|
+
dataflow: {
|
|
47
|
+
tracker,
|
|
48
|
+
sinks: { isVulnerable, reportFindings },
|
|
49
|
+
eventFactory: { createSinkEvent },
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
} = core;
|
|
53
|
+
|
|
54
|
+
const instr = core.assess.dataflow.sinks.marsdb = {};
|
|
55
|
+
const inspect = patcher.unwrap(util.inspect);
|
|
56
|
+
|
|
57
|
+
function getVulnerabilityInfo(query) {
|
|
58
|
+
let vulnInfo = null;
|
|
59
|
+
if (!query) return vulnInfo;
|
|
60
|
+
|
|
61
|
+
traverseValues(query, (path, type, value) => {
|
|
62
|
+
const strInfo = tracker.getData(value);
|
|
63
|
+
if (strInfo && isVulnerable(UNTRUSTED, querySafeTags, strInfo.tags)) {
|
|
64
|
+
vulnInfo = { path, strInfo };
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return vulnInfo;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function patchCollection(marsdb, method) {
|
|
73
|
+
const proto = marsdb.Collection.prototype;
|
|
74
|
+
const name = `marsdb.Collection.prototype.${method}`;
|
|
75
|
+
|
|
76
|
+
if (!proto[method]) {
|
|
77
|
+
logger.trace({ name }, `marsdb method ${method} not found!`);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
patcher.patch(proto, method, {
|
|
82
|
+
name,
|
|
83
|
+
patchType,
|
|
84
|
+
around(next, data) {
|
|
85
|
+
const sourceCtx = sources.getStore()?.assess;
|
|
86
|
+
if (!sourceCtx || instrumentation.isLocked()) {
|
|
87
|
+
return next();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const argIdx = 0;
|
|
91
|
+
const result = getVulnerabilityInfo(data.args[argIdx]);
|
|
92
|
+
if (!result) {
|
|
93
|
+
return next();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const { strInfo } = result;
|
|
97
|
+
const args = data.args.map((arg, idx) => ({
|
|
98
|
+
value: inspect(arg),
|
|
99
|
+
tracked: idx === argIdx,
|
|
100
|
+
}));
|
|
101
|
+
|
|
102
|
+
const sinkEvent = createSinkEvent({
|
|
103
|
+
args,
|
|
104
|
+
context: `marsdb.Collection.${method}(${args.map((a) => a.value)})`,
|
|
105
|
+
history: [strInfo],
|
|
106
|
+
object: {
|
|
107
|
+
tracked: false,
|
|
108
|
+
value: 'marsdb.Collection',
|
|
109
|
+
},
|
|
110
|
+
name,
|
|
111
|
+
result: strInfo.result,
|
|
112
|
+
source: `P${argIdx}`,
|
|
113
|
+
stacktraceOpts: {
|
|
114
|
+
constructorOpt: data.hooked,
|
|
115
|
+
},
|
|
116
|
+
tags: strInfo.tags,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
if (sinkEvent) {
|
|
120
|
+
reportFindings({ ruleId: Rule.NOSQL_INJECTION_MONGO, sinkEvent });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return next();
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
instr.install = function() {
|
|
129
|
+
depHooks.resolve({ name: 'marsdb' }, (marsdb) => {
|
|
130
|
+
collectionMethods.forEach((method) => patchCollection(marsdb, method));
|
|
131
|
+
});
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
return instr;
|
|
135
|
+
};
|
|
@@ -0,0 +1,205 @@
|
|
|
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 util = require('util');
|
|
19
|
+
const {
|
|
20
|
+
DataflowTag: {
|
|
21
|
+
UNTRUSTED,
|
|
22
|
+
ALPHANUM_SPACE_HYPHEN,
|
|
23
|
+
CUSTOM_VALIDATED_NOSQL_INJECTION,
|
|
24
|
+
LIMITED_CHARS,
|
|
25
|
+
STRING_TYPE_CHECKED,
|
|
26
|
+
},
|
|
27
|
+
Rule,
|
|
28
|
+
isNonEmptyObject,
|
|
29
|
+
traverseValues
|
|
30
|
+
} = require('@contrast/common');
|
|
31
|
+
const utils = require('../../tag-utils');
|
|
32
|
+
const { patchType } = require('../common');
|
|
33
|
+
|
|
34
|
+
const collectionMethods = [
|
|
35
|
+
'find',
|
|
36
|
+
'findOne',
|
|
37
|
+
'findAndModify',
|
|
38
|
+
'findOneAndDelete',
|
|
39
|
+
'findOneAndReplace',
|
|
40
|
+
'findOneAndUpdate',
|
|
41
|
+
'remove',
|
|
42
|
+
'replaceOne',
|
|
43
|
+
'update',
|
|
44
|
+
'updateOne',
|
|
45
|
+
'updateMany',
|
|
46
|
+
'deleteOne',
|
|
47
|
+
'deleteMany',
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
const querySafeTags = [
|
|
51
|
+
ALPHANUM_SPACE_HYPHEN,
|
|
52
|
+
CUSTOM_VALIDATED_NOSQL_INJECTION,
|
|
53
|
+
LIMITED_CHARS,
|
|
54
|
+
STRING_TYPE_CHECKED,
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
module.exports = function(core) {
|
|
58
|
+
const {
|
|
59
|
+
depHooks,
|
|
60
|
+
logger,
|
|
61
|
+
patcher,
|
|
62
|
+
scopes: { sources, instrumentation },
|
|
63
|
+
assess: {
|
|
64
|
+
dataflow: {
|
|
65
|
+
tracker,
|
|
66
|
+
sinks: { isVulnerable, reportFindings },
|
|
67
|
+
eventFactory: { createSinkEvent }
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
} = core;
|
|
71
|
+
|
|
72
|
+
const inspect = patcher.unwrap(util.inspect);
|
|
73
|
+
const instr = core.assess.dataflow.sinks.mongodb = {};
|
|
74
|
+
|
|
75
|
+
instr.getVulnerabilityInfo = function getVulnerabilityInfo(query) {
|
|
76
|
+
let vulnInfo = null;
|
|
77
|
+
|
|
78
|
+
if (!isNonEmptyObject(query)) return vulnInfo;
|
|
79
|
+
|
|
80
|
+
traverseValues(query, (path, type, value) => {
|
|
81
|
+
const strInfo = tracker.getData(value);
|
|
82
|
+
if (strInfo && isVulnerable(UNTRUSTED, querySafeTags, strInfo.tags)) {
|
|
83
|
+
vulnInfo = { path, strInfo };
|
|
84
|
+
return true; // halts traversal
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return vulnInfo;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
instr.install = function() {
|
|
92
|
+
depHooks.resolve({ name: 'mongodb' }, (mongodb, version) => {
|
|
93
|
+
patchCollection(mongodb, version);
|
|
94
|
+
patchDatabase(mongodb, version);
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return instr;
|
|
99
|
+
|
|
100
|
+
function patchCollection(mongodb, version) {
|
|
101
|
+
for (const method of collectionMethods) {
|
|
102
|
+
|
|
103
|
+
const proto = mongodb.Collection.prototype;
|
|
104
|
+
const name = `mongodb.Collection.prototype.${method}`;
|
|
105
|
+
|
|
106
|
+
if (!proto[method]) {
|
|
107
|
+
|
|
108
|
+
logger.trace({ name, version }, 'method not found - skipping instrumentation');
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
patcher.patch(proto, method, {
|
|
113
|
+
name,
|
|
114
|
+
patchType,
|
|
115
|
+
around(next, data) {
|
|
116
|
+
const { obj, args } = data;
|
|
117
|
+
const sourceCtx = sources.getStore()?.assess;
|
|
118
|
+
|
|
119
|
+
if (instrumentation.isLocked() || !sourceCtx) {
|
|
120
|
+
return next();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const argIdx = 0;
|
|
124
|
+
try {
|
|
125
|
+
const vulnInfo = instr.getVulnerabilityInfo(args[argIdx]);
|
|
126
|
+
if (vulnInfo) {
|
|
127
|
+
const { path, strInfo } = vulnInfo;
|
|
128
|
+
const objName = getObjectName(obj);
|
|
129
|
+
const args = data.args.map((arg, idx) => ({
|
|
130
|
+
value: inspect(arg),
|
|
131
|
+
tracked: idx === argIdx,
|
|
132
|
+
}));
|
|
133
|
+
|
|
134
|
+
const tags = getAdjustedQueryTags(path, strInfo, args[argIdx].value);
|
|
135
|
+
const resultVal = args[args.length - 1].value.startsWith('[Function') ? '' : 'Promise';
|
|
136
|
+
const sinkEvent = createSinkEvent({
|
|
137
|
+
args,
|
|
138
|
+
context: `${objName}.${method}(${args.map((a) => a.value)})`,
|
|
139
|
+
history: [strInfo],
|
|
140
|
+
object: {
|
|
141
|
+
tracked: false,
|
|
142
|
+
value: 'mongodb.Collection',
|
|
143
|
+
},
|
|
144
|
+
name,
|
|
145
|
+
result: {
|
|
146
|
+
tracked: false,
|
|
147
|
+
value: resultVal,
|
|
148
|
+
},
|
|
149
|
+
source: `P${argIdx}`,
|
|
150
|
+
stacktraceOpts: {
|
|
151
|
+
constructorOpt: data.hooked,
|
|
152
|
+
},
|
|
153
|
+
tags,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
if (sinkEvent) {
|
|
157
|
+
reportFindings({ ruleId: Rule.NOSQL_INJECTION_MONGO, sinkEvent });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
} catch (err) {
|
|
161
|
+
core.logger.error({ name, err }, 'assess sink analysis failed');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (method === 'findOne') {
|
|
165
|
+
// `findOne` will call `find` so don't analyze in nested call
|
|
166
|
+
const store = { name, lock: true };
|
|
167
|
+
const ret = instrumentation.run(store, next);
|
|
168
|
+
// but unlock for when callback args run or returned Promises resolve/reject
|
|
169
|
+
store.lock = false;
|
|
170
|
+
return ret;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return next();
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
function patchDatabase(mongodb, version) {
|
|
181
|
+
// todo
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function getObjectName(obj) {
|
|
185
|
+
let name = '';
|
|
186
|
+
name += obj.s?.namespace?.db || 'db';
|
|
187
|
+
name += '.';
|
|
188
|
+
name += obj.s?.namespace?.collection || 'collection';
|
|
189
|
+
return name;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function getAdjustedQueryTags(path, strInfo, argString) {
|
|
193
|
+
const { tags } = strInfo;
|
|
194
|
+
let idx = -1;
|
|
195
|
+
for (const str of [...path, strInfo.value]) {
|
|
196
|
+
idx = argString.indexOf(str, idx);
|
|
197
|
+
if (idx == -1) {
|
|
198
|
+
idx = -1;
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return idx > 0 ? utils.createAppendTags([], tags, idx) : strInfo.tags;
|
|
204
|
+
}
|
|
205
|
+
};
|
|
@@ -15,15 +15,19 @@
|
|
|
15
15
|
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
|
-
const {
|
|
18
|
+
const {
|
|
19
|
+
DataflowTag: { UNTRUSTED, SQL_ENCODED, LIMITED_CHARS, CUSTOM_VALIDATED, CUSTOM_ENCODED },
|
|
20
|
+
Rule,
|
|
21
|
+
isString
|
|
22
|
+
} = require('@contrast/common');
|
|
19
23
|
const { createModuleLabel } = require('../../propagation/common');
|
|
20
24
|
const { patchType } = require('../common');
|
|
21
25
|
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
const safeTags = [
|
|
27
|
+
SQL_ENCODED,
|
|
28
|
+
LIMITED_CHARS,
|
|
29
|
+
CUSTOM_VALIDATED,
|
|
30
|
+
CUSTOM_ENCODED,
|
|
27
31
|
];
|
|
28
32
|
|
|
29
33
|
module.exports = function (core) {
|
|
@@ -45,7 +49,7 @@ module.exports = function (core) {
|
|
|
45
49
|
if (!store || !data.args[0] || !isString(data.args[0])) return;
|
|
46
50
|
|
|
47
51
|
const strInfo = tracker.getData(data.args[0]);
|
|
48
|
-
if (!strInfo || !isVulnerable(
|
|
52
|
+
if (!strInfo || !isVulnerable(UNTRUSTED, safeTags, strInfo.tags)) {
|
|
49
53
|
return;
|
|
50
54
|
}
|
|
51
55
|
|