@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.
@@ -16,7 +16,17 @@
16
16
  'use strict';
17
17
 
18
18
  const util = require('util');
19
- const { isString } = require('@contrast/common');
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
- 'limited-chars',
43
- 'url-encoded',
44
- 'html-encoded',
45
- 'custom-validated',
46
- 'custom-encoded'
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(requiredTag, safeTags, urlPathTags)) {
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 { Rule } = require('@contrast/common');
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
- 'alphanum-space-hyphen',
38
- 'cookie',
39
- 'header',
40
- 'limited-chars',
41
- 'html-encoded',
42
- 'sql-encoded',
43
- 'url-encoded',
44
- 'weak-url-encoded',
45
- 'custom-validated',
46
- 'custom-encoded'
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(requiredTag, safeTags, strInfo.tags)) {
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 { isString } = require('@contrast/common');
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
- 'limited-chars',
43
- 'url-encoded',
44
- 'html-encoded',
45
- 'custom-validated',
46
- 'custom-encoded'
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(requiredTag, safeTags, urlPathTags)) {
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 { Rule, isString } = require('@contrast/common');
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 SAFE_TAGS = [
23
- 'sql-encoded',
24
- 'limited-chars',
25
- 'custom-validated',
26
- 'custom-encoded',
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('untrusted', SAFE_TAGS, strInfo.tags)) {
52
+ if (!strInfo || !isVulnerable(UNTRUSTED, safeTags, strInfo.tags)) {
49
53
  return;
50
54
  }
51
55