@contrast/assess 1.5.0 → 1.7.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.
Files changed (40) hide show
  1. package/lib/dataflow/event-factory.js +10 -5
  2. package/lib/dataflow/propagation/index.js +1 -0
  3. package/lib/dataflow/propagation/install/contrast-methods/string.js +5 -1
  4. package/lib/dataflow/propagation/install/decode-uri-component.js +5 -2
  5. package/lib/dataflow/propagation/install/ejs/escape-xml.js +5 -2
  6. package/lib/dataflow/propagation/install/encode-uri-component.js +5 -2
  7. package/lib/dataflow/propagation/install/escape-html.js +7 -4
  8. package/lib/dataflow/propagation/install/escape.js +5 -2
  9. package/lib/dataflow/propagation/install/handlebars-utils-escape-expression.js +5 -2
  10. package/lib/dataflow/propagation/install/mysql-connection-escape.js +5 -2
  11. package/lib/dataflow/propagation/install/pug-runtime-escape.js +5 -2
  12. package/lib/dataflow/propagation/install/querystring/parse.js +8 -3
  13. package/lib/dataflow/propagation/install/sequelize.js +310 -0
  14. package/lib/dataflow/propagation/install/sql-template-strings.js +5 -4
  15. package/lib/dataflow/propagation/install/string/match.js +2 -2
  16. package/lib/dataflow/propagation/install/string/replace.js +13 -4
  17. package/lib/dataflow/propagation/install/unescape.js +5 -2
  18. package/lib/dataflow/propagation/install/validator/methods.js +60 -51
  19. package/lib/dataflow/sinks/common.js +10 -1
  20. package/lib/dataflow/sinks/index.js +34 -1
  21. package/lib/dataflow/sinks/install/child-process.js +150 -13
  22. package/lib/dataflow/sinks/install/express/index.js +29 -0
  23. package/lib/dataflow/sinks/install/express/unvalidated-redirect.js +134 -0
  24. package/lib/dataflow/sinks/install/fastify/unvalidated-redirect.js +113 -75
  25. package/lib/dataflow/sinks/install/fs.js +136 -0
  26. package/lib/dataflow/sinks/install/http.js +46 -17
  27. package/lib/dataflow/sinks/install/koa/unvalidated-redirect.js +50 -17
  28. package/lib/dataflow/sinks/install/marsdb.js +135 -0
  29. package/lib/dataflow/sinks/install/mongodb.js +322 -0
  30. package/lib/dataflow/sinks/install/mssql.js +19 -10
  31. package/lib/dataflow/sinks/install/mysql.js +138 -0
  32. package/lib/dataflow/sinks/install/postgres.js +37 -23
  33. package/lib/dataflow/sinks/install/sequelize.js +142 -0
  34. package/lib/dataflow/sinks/install/sqlite3.js +20 -10
  35. package/lib/dataflow/sources/handler.js +14 -9
  36. package/lib/dataflow/sources/index.js +4 -1
  37. package/lib/dataflow/sources/install/body-parser1.js +120 -0
  38. package/lib/dataflow/sources/install/cookie-parser1.js +101 -0
  39. package/lib/dataflow/sources/install/express/index.js +28 -0
  40. package/package.json +3 -3
@@ -16,19 +16,50 @@
16
16
  'use strict';
17
17
 
18
18
  const util = require('util');
19
- const { isString } = require('@contrast/common');
20
- const { patchType } = require('../../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');
21
30
  const { createSubsetTags } = require('../../../tag-utils');
31
+ const { filterSafeTags, patchType } = require('../../common');
32
+
33
+ const ruleId = 'unvalidated-redirect';
34
+ const getURLArgument = (args) => {
35
+ if (!Array.isArray(args)) {
36
+ return { index: null, url: undefined };
37
+ }
38
+
39
+ // url can be first or second argument
40
+ if (typeof args[0] === 'string') {
41
+ return {
42
+ index: 0,
43
+ url: args[0]
44
+ };
45
+ }
46
+
47
+ return {
48
+ index: 1,
49
+ url: args[1]
50
+ };
51
+ };
22
52
 
23
53
  module.exports = function (core) {
24
54
  const {
55
+ config,
25
56
  depHooks,
26
57
  patcher,
27
58
  scopes: { sources },
28
59
  assess: {
29
60
  dataflow: {
30
61
  tracker,
31
- sinks: { isVulnerable, reportFindings },
62
+ sinks: { isVulnerable, reportFindings, reportSafePositive },
32
63
  eventFactory: { createSinkEvent },
33
64
  },
34
65
  },
@@ -39,85 +70,92 @@ module.exports = function (core) {
39
70
  const inspect = patcher.unwrap(util.inspect);
40
71
 
41
72
  const safeTags = [
42
- 'limited-chars',
43
- 'url-encoded',
44
- 'html-encoded',
45
- 'custom-validated',
46
- 'custom-encoded'
73
+ CUSTOM_ENCODED,
74
+ CUSTOM_VALIDATED,
75
+ HTML_ENCODED,
76
+ LIMITED_CHARS,
77
+ URL_ENCODED,
47
78
  ];
48
- const requiredTag = 'untrusted';
49
-
50
- /**
51
- * Patches `Reply.prototype.redirect` for
52
- * `unvalidated-redirect` checking
53
- *
54
- * @param {Fastify.Reply} Reply outgoing response
55
- */
56
- const registerUnvalidatedRedirectHandler = (Reply, version) => {
57
- patcher.patch(Reply.prototype, 'redirect', {
58
- name: 'fastify.Reply.prototype.redirect',
59
- patchType,
60
- post(data) {
61
- const assessStore = sources.getStore()?.assess;
62
- if (!assessStore) return;
63
-
64
- const [code] = data.args;
65
- // url can be first or second argument
66
- const url = typeof code === 'string' ? code : data.args[1];
67
-
68
- if (!url || !isString(url)) return;
69
-
70
- const strInfo = tracker.getData(url);
71
- if (!strInfo) return;
72
-
73
- let urlPathTags = strInfo.tags;
74
- const urlPathEndIdx = url.indexOf('?');
75
-
76
- if (urlPathEndIdx > -1) {
77
- urlPathTags = createSubsetTags(strInfo.tags, 0, urlPathEndIdx);
78
- }
79
-
80
- if (isVulnerable(requiredTag, safeTags, urlPathTags)) {
81
- const event = createSinkEvent({
82
- args: [{
79
+
80
+ unvalidatedRedirect.install = function () {
81
+ const name = 'fastify.Reply.prototype.redirect';
82
+ depHooks.resolve({ name: 'fastify', file: 'lib/reply' }, (Reply, version) => {
83
+ patcher.patch(Reply.prototype, 'redirect', {
84
+ name,
85
+ patchType,
86
+ post(data) {
87
+ const assessStore = sources.getStore()?.assess;
88
+ if (!assessStore) return;
89
+
90
+ const { url, index: valueIndex } = getURLArgument(data.args);
91
+ if (!url || !isString(url)) return;
92
+
93
+ const strInfo = tracker.getData(url);
94
+ if (!strInfo) return;
95
+
96
+ // todo: how does different tag logic play into display ranges?
97
+
98
+ let urlPathTags = strInfo.tags;
99
+ const urlPathEndIdx = url.indexOf('?');
100
+ if (urlPathEndIdx > -1) {
101
+ urlPathTags = createSubsetTags(strInfo.tags, 0, urlPathEndIdx);
102
+ }
103
+
104
+ if (isVulnerable(UNTRUSTED, safeTags, urlPathTags)) {
105
+ const args = [];
106
+ // in case a status code is provided
107
+ if (valueIndex) {
108
+ args.push({
109
+ tracked: false,
110
+ value: data.args[0]
111
+ });
112
+ }
113
+ args.push({
83
114
  tracked: true,
84
115
  value: strInfo.value,
85
- }],
86
- context: `reply.redirect(${inspect(strInfo.value)})`,
87
- history: [strInfo],
88
- name: 'fastify.reply.redirect',
89
- object: {
90
- tracked: false,
91
- value: 'fastify.Reply',
92
- },
93
- result: {
94
- tracked: false,
95
- value: undefined,
96
- },
97
- tags: urlPathTags,
98
- source: 'P0',
99
- stacktraceOpts: {
100
- constructorOpt: data.hooked,
101
- },
102
- });
103
-
104
- if (event) {
105
- reportFindings({
106
- ruleId: 'unvalidated-redirect', // add Rule.UNVALIDATED_REDIRECT
107
- sinkEvent: event,
116
+ });
117
+
118
+ const event = createSinkEvent({
119
+ args,
120
+ context: `reply.redirect(${inspect(strInfo.value)})`,
121
+ history: [strInfo],
122
+ name: 'fastify.reply.redirect',
123
+ object: {
124
+ tracked: false,
125
+ value: 'fastify.Reply',
126
+ },
127
+ result: {
128
+ tracked: false,
129
+ value: undefined,
130
+ },
131
+ tags: urlPathTags,
132
+ source: `P${valueIndex}`,
133
+ stacktraceOpts: {
134
+ constructorOpt: data.hooked,
135
+ },
136
+ });
137
+
138
+ if (event) {
139
+ reportFindings({
140
+ ruleId,
141
+ sinkEvent: event,
142
+ });
143
+ }
144
+ } else if (config.assess.safe_positives.enable) {
145
+ reportSafePositive({
146
+ name,
147
+ ruleId,
148
+ safeTags: filterSafeTags(safeTags, strInfo),
149
+ strInfo: {
150
+ tags: strInfo.tags,
151
+ value: strInfo.value,
152
+ }
108
153
  });
109
154
  }
110
- }
111
- },
155
+ },
156
+ });
112
157
  });
113
158
  };
114
159
 
115
- unvalidatedRedirect.install = function () {
116
- depHooks.resolve(
117
- { name: 'fastify', file: 'lib/reply' },
118
- registerUnvalidatedRedirectHandler
119
- );
120
- };
121
-
122
160
  return unvalidatedRedirect;
123
161
  };
@@ -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,18 +15,39 @@
15
15
 
16
16
  'use strict';
17
17
 
18
- const { Rule } = require('@contrast/common');
19
- const { patchType } = require('../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: { REFLECTED_XSS: ruleId },
33
+ } = require('@contrast/common');
34
+ const { patchType, filterSafeTags } = require('../common');
20
35
 
21
36
  module.exports = function(core) {
22
37
  const {
38
+ config,
23
39
  depHooks,
24
40
  patcher,
25
41
  scopes: { sources },
26
42
  assess: {
27
43
  dataflow: {
28
44
  tracker,
29
- sinks: { isVulnerable, reportFindings, isSafeContentType },
45
+ sinks: {
46
+ isVulnerable,
47
+ reportFindings,
48
+ reportSafePositive,
49
+ isSafeContentType
50
+ },
30
51
  eventFactory: { createSinkEvent },
31
52
  },
32
53
  },
@@ -34,18 +55,17 @@ module.exports = function(core) {
34
55
  const http = core.assess.dataflow.sinks.http = {};
35
56
 
36
57
  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'
58
+ ALPHANUM_SPACE_HYPHEN,
59
+ COOKIE,
60
+ CUSTOM_ENCODED,
61
+ CUSTOM_VALIDATED,
62
+ HEADER,
63
+ HTML_ENCODED,
64
+ LIMITED_CHARS,
65
+ SQL_ENCODED,
66
+ URL_ENCODED,
67
+ WEAK_URL_ENCODED,
47
68
  ];
48
- const requiredTag = 'untrusted';
49
69
 
50
70
  const preHook = (name, method) => (data) => {
51
71
  const sourceContext = sources.getStore()?.assess;
@@ -60,7 +80,7 @@ module.exports = function(core) {
60
80
  const { contentType } = sourceContext.responseData;
61
81
  if (contentType && isSafeContentType(contentType)) return;
62
82
 
63
- if (isVulnerable(requiredTag, safeTags, strInfo.tags)) {
83
+ if (isVulnerable(UNTRUSTED, safeTags, strInfo.tags)) {
64
84
  const event = createSinkEvent({
65
85
  args: [{
66
86
  value: strInfo.value,
@@ -77,7 +97,6 @@ module.exports = function(core) {
77
97
  value: data.result,
78
98
  tracked: false,
79
99
  },
80
- ruleId: Rule.REFLECTED_XSS,
81
100
  source: 'P0',
82
101
  stacktraceOpts: {
83
102
  constructorOpt: data.hooked
@@ -87,10 +106,20 @@ module.exports = function(core) {
87
106
 
88
107
  if (event) {
89
108
  reportFindings({
90
- ruleId: 'reflected-xss',
109
+ ruleId,
91
110
  sinkEvent: event
92
111
  });
93
112
  }
113
+ } else if (config.assess.safe_positives.enable) {
114
+ reportSafePositive({
115
+ name,
116
+ ruleId,
117
+ safeTags: filterSafeTags(safeTags, strInfo),
118
+ strInfo: {
119
+ value: strInfo.value,
120
+ tags: strInfo.tags,
121
+ }
122
+ });
94
123
  }
95
124
  };
96
125
 
@@ -16,19 +16,32 @@
16
16
  'use strict';
17
17
 
18
18
  const util = require('util');
19
- const { isString } = require('@contrast/common');
20
- const { patchType } = require('../../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');
21
30
  const { createSubsetTags } = require('../../../tag-utils');
31
+ const { filterSafeTags, patchType } = require('../../common');
32
+
33
+ const ruleId = 'unvalidated-redirect';
22
34
 
23
35
  module.exports = function (core) {
24
36
  const {
25
37
  depHooks,
26
38
  patcher,
39
+ config,
27
40
  scopes: { sources },
28
41
  assess: {
29
42
  dataflow: {
30
43
  tracker,
31
- sinks: { isVulnerable, reportFindings },
44
+ sinks: { isVulnerable, reportFindings, reportSafePositive },
32
45
  eventFactory: { createSinkEvent },
33
46
  },
34
47
  },
@@ -39,16 +52,16 @@ module.exports = function (core) {
39
52
  const inspect = patcher.unwrap(util.inspect);
40
53
 
41
54
  const safeTags = [
42
- 'limited-chars',
43
- 'url-encoded',
44
- 'html-encoded',
45
- 'custom-validated',
46
- 'custom-encoded'
55
+ CUSTOM_ENCODED,
56
+ CUSTOM_VALIDATED,
57
+ HTML_ENCODED,
58
+ LIMITED_CHARS,
59
+ URL_ENCODED,
47
60
  ];
48
- const requiredTag = 'untrusted';
49
61
 
50
62
  unvalidatedRedirect.install = function () {
51
63
  depHooks.resolve({ name: 'koa', file: 'lib/response', version: '<2.9.0' }, (Response) => {
64
+ const name = 'Koa.Response.redirect';
52
65
  patcher.patch(Response, 'redirect', {
53
66
  name: 'Koa.Response.redirect',
54
67
  patchType,
@@ -56,9 +69,10 @@ module.exports = function (core) {
56
69
  const assessStore = sources.getStore()?.assess;
57
70
  if (!assessStore) return;
58
71
 
72
+ let isBackRoute = false;
59
73
  let [url] = data.args;
60
-
61
74
  if (url === 'back') {
75
+ isBackRoute = true;
62
76
  url = data.obj.ctx.get('Referrer') || data.args[1];
63
77
  }
64
78
 
@@ -73,12 +87,21 @@ module.exports = function (core) {
73
87
  urlPathTags = createSubsetTags(strInfo.tags, 0, url.indexOf('?'));
74
88
  }
75
89
 
76
- if (urlPathTags && isVulnerable(requiredTag, safeTags, urlPathTags)) {
90
+ if (urlPathTags && isVulnerable(UNTRUSTED, safeTags, urlPathTags)) {
91
+ const args = [];
92
+ if (isBackRoute) {
93
+ args.push({
94
+ tracked: false,
95
+ value: data.args[0]
96
+ });
97
+ }
98
+ args.push({
99
+ tracked: true,
100
+ value: strInfo.value,
101
+ });
102
+
77
103
  const event = createSinkEvent({
78
- args: [{
79
- tracked: true,
80
- value: strInfo.value,
81
- }],
104
+ args,
82
105
  context: `response.redirect(${inspect(strInfo.value)})`,
83
106
  history: [strInfo],
84
107
  name: 'Koa.Response.redirect',
@@ -91,7 +114,7 @@ module.exports = function (core) {
91
114
  value: undefined,
92
115
  },
93
116
  tags: urlPathTags,
94
- source: 'P0',
117
+ source: `P${isBackRoute ? 1 : 0}`,
95
118
  stacktraceOpts: {
96
119
  constructorOpt: data.hooked,
97
120
  },
@@ -99,10 +122,20 @@ module.exports = function (core) {
99
122
 
100
123
  if (event) {
101
124
  reportFindings({
102
- ruleId: 'unvalidated-redirect',
125
+ ruleId,
103
126
  sinkEvent: event,
104
127
  });
105
128
  }
129
+ } else if (config.assess.safe_positives.enable) {
130
+ reportSafePositive({
131
+ name,
132
+ ruleId,
133
+ safeTags: filterSafeTags(safeTags, strInfo),
134
+ strInfo: {
135
+ tags: strInfo.tags,
136
+ value: strInfo.value,
137
+ }
138
+ });
106
139
  }
107
140
  }
108
141
  });