@contrast/assess 1.8.0 → 1.9.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 (62) hide show
  1. package/lib/dataflow/event-factory.js +17 -13
  2. package/lib/dataflow/propagation/index.js +3 -0
  3. package/lib/dataflow/propagation/install/JSON/index.js +1 -0
  4. package/lib/dataflow/propagation/install/JSON/parse-fn.js +248 -0
  5. package/lib/dataflow/propagation/install/JSON/parse.js +196 -0
  6. package/lib/dataflow/propagation/install/JSON/stringify.js +5 -3
  7. package/lib/dataflow/propagation/install/array-prototype-join.js +21 -14
  8. package/lib/dataflow/propagation/install/buffer.js +2 -0
  9. package/lib/dataflow/propagation/install/contrast-methods/add.js +2 -0
  10. package/lib/dataflow/propagation/install/contrast-methods/index.js +1 -0
  11. package/lib/dataflow/propagation/install/contrast-methods/number.js +58 -0
  12. package/lib/dataflow/propagation/install/contrast-methods/string.js +53 -6
  13. package/lib/dataflow/propagation/install/contrast-methods/tag.js +3 -1
  14. package/lib/dataflow/propagation/install/decode-uri-component.js +9 -2
  15. package/lib/dataflow/propagation/install/ejs/escape-xml.js +8 -2
  16. package/lib/dataflow/propagation/install/encode-uri-component.js +9 -2
  17. package/lib/dataflow/propagation/install/escape-html.js +13 -5
  18. package/lib/dataflow/propagation/install/escape.js +9 -2
  19. package/lib/dataflow/propagation/install/handlebars-utils-escape-expression.js +11 -4
  20. package/lib/dataflow/propagation/install/isnumeric-0.js +59 -0
  21. package/lib/dataflow/propagation/install/mongoose/index.js +36 -0
  22. package/lib/dataflow/propagation/install/mongoose/schema-string.js +156 -0
  23. package/lib/dataflow/propagation/install/mysql-connection-escape.js +5 -0
  24. package/lib/dataflow/propagation/install/parse-int.js +60 -0
  25. package/lib/dataflow/propagation/install/pug-runtime-escape.js +9 -2
  26. package/lib/dataflow/propagation/install/querystring/parse.js +11 -9
  27. package/lib/dataflow/propagation/install/sequelize.js +6 -3
  28. package/lib/dataflow/propagation/install/sql-template-strings.js +9 -2
  29. package/lib/dataflow/propagation/install/string/concat.js +8 -2
  30. package/lib/dataflow/propagation/install/string/format-methods.js +7 -2
  31. package/lib/dataflow/propagation/install/string/html-methods.js +15 -5
  32. package/lib/dataflow/propagation/install/string/match.js +14 -9
  33. package/lib/dataflow/propagation/install/string/replace.js +22 -14
  34. package/lib/dataflow/propagation/install/string/slice.js +13 -5
  35. package/lib/dataflow/propagation/install/string/split.js +15 -11
  36. package/lib/dataflow/propagation/install/string/substring.js +16 -6
  37. package/lib/dataflow/propagation/install/string/trim.js +3 -0
  38. package/lib/dataflow/propagation/install/unescape.js +9 -2
  39. package/lib/dataflow/propagation/install/url/domain-parsers.js +7 -2
  40. package/lib/dataflow/propagation/install/validator/hooks.js +6 -2
  41. package/lib/dataflow/sinks/install/child-process.js +116 -50
  42. package/lib/dataflow/sinks/install/express/unvalidated-redirect.js +6 -3
  43. package/lib/dataflow/sinks/install/fastify/unvalidated-redirect.js +7 -4
  44. package/lib/dataflow/sinks/install/fs.js +44 -12
  45. package/lib/dataflow/sinks/install/http.js +5 -2
  46. package/lib/dataflow/sinks/install/koa/unvalidated-redirect.js +7 -4
  47. package/lib/dataflow/sinks/install/marsdb.js +3 -0
  48. package/lib/dataflow/sinks/install/mongodb.js +3 -1
  49. package/lib/dataflow/sinks/install/mssql.js +9 -2
  50. package/lib/dataflow/sinks/install/mysql.js +9 -4
  51. package/lib/dataflow/sinks/install/postgres.js +6 -3
  52. package/lib/dataflow/sinks/install/sequelize.js +7 -5
  53. package/lib/dataflow/sinks/install/sqlite3.js +7 -3
  54. package/lib/dataflow/sources/handler.js +2 -1
  55. package/lib/dataflow/sources/install/http.js +1 -1
  56. package/lib/dataflow/tag-utils.js +25 -1
  57. package/lib/dataflow/tracker.js +6 -6
  58. package/lib/index.js +2 -0
  59. package/lib/response-scanning/handlers/utils.js +2 -2
  60. package/lib/session-configuration/index.js +34 -0
  61. package/lib/session-configuration/install/http.js +79 -0
  62. package/package.json +2 -2
@@ -28,6 +28,7 @@ const {
28
28
  LIMITED_CHARS,
29
29
  UNTRUSTED
30
30
  },
31
+ inspect
31
32
  } = require('@contrast/common');
32
33
 
33
34
  const safeTags = [
@@ -39,7 +40,7 @@ const safeTags = [
39
40
  LIMITED_CHARS,
40
41
  ];
41
42
 
42
- module.exports = function (core) {
43
+ module.exports = function(core) {
43
44
  const {
44
45
  depHooks,
45
46
  patcher,
@@ -63,7 +64,7 @@ module.exports = function (core) {
63
64
  }
64
65
  }
65
66
 
66
- const pre = (module, file, obj) => (data) => {
67
+ const pre = (module, file, obj, method) => (data) => {
67
68
  const store = sources.getStore()?.assess;
68
69
  if (
69
70
  !store ||
@@ -81,6 +82,9 @@ module.exports = function (core) {
81
82
 
82
83
  const event = createSinkEvent({
83
84
  name: `${module}/${file}`,
85
+ moduleName: module,
86
+ methodName: `prototype.${method}`,
87
+ context: `${module}.${method}(${inspect(data.args[0])})`,
84
88
  history: [strInfo],
85
89
  object: {
86
90
  value: `${module}.${obj}`,
@@ -96,6 +100,7 @@ module.exports = function (core) {
96
100
  source: 'P0',
97
101
  stacktraceOpts: {
98
102
  contructorOpt: data.hooked,
103
+ prependFrames: [data.orig]
99
104
  },
100
105
  });
101
106
 
@@ -115,7 +120,7 @@ module.exports = function (core) {
115
120
  patcher.patch(Connection.prototype, 'query', {
116
121
  name: 'Connection.prototype.query',
117
122
  patchType,
118
- pre: pre('mysql', 'lib/Connection.query', 'Connection')
123
+ pre: pre('mysql', 'lib/Connection.query', 'Connection', 'query')
119
124
  });
120
125
  },
121
126
  );
@@ -126,7 +131,7 @@ module.exports = function (core) {
126
131
  patcher.patch(connection.prototype, `${method}`, {
127
132
  name: `connection.prototype.${method}`,
128
133
  patchType,
129
- pre: pre('mysql2', `lib/connection.Connection.${method}`, 'connection')
134
+ pre: pre('mysql2', `lib/connection.Connection.${method}`, 'connection', method)
130
135
  });
131
136
  });
132
137
  },
@@ -19,11 +19,11 @@ const util = require('util');
19
19
  const {
20
20
  DataflowTag: { UNTRUSTED, SQL_ENCODED, LIMITED_CHARS, CUSTOM_VALIDATED, CUSTOM_ENCODED },
21
21
  Rule: { SQL_INJECTION: ruleId },
22
- isString
22
+ isString,
23
23
  } = require('@contrast/common');
24
24
  const { filterSafeTags, patchType } = require('../common');
25
25
 
26
- module.exports = function (core) {
26
+ module.exports = function(core) {
27
27
  const {
28
28
  config,
29
29
  depHooks,
@@ -74,6 +74,8 @@ module.exports = function (core) {
74
74
  context: `${objValue}.query(${arg0Val})`,
75
75
  history: [strInfo],
76
76
  name: methodSignature,
77
+ moduleName: `${methodSignature.includes('pool') ? 'pg-pool' : 'pg'}`,
78
+ methodName: 'Connection.prototype.query',
77
79
  object: {
78
80
  tracked: false,
79
81
  value: objValue,
@@ -86,6 +88,7 @@ module.exports = function (core) {
86
88
  source: 'P0',
87
89
  stacktraceOpts: {
88
90
  constructorOpt: data.hooked,
91
+ prependFrames: [data.orig]
89
92
  },
90
93
  });
91
94
 
@@ -108,7 +111,7 @@ module.exports = function (core) {
108
111
  }
109
112
  };
110
113
 
111
- postgres.install = function () {
114
+ postgres.install = function() {
112
115
  const pgClientQueryPatchName = 'pg.Client.prototype.query';
113
116
  depHooks.resolve(
114
117
  { name: 'pg', file: 'lib/client.js' },
@@ -28,7 +28,7 @@ const {
28
28
  } = require('@contrast/common');
29
29
  const { patchType, filterSafeTags } = require('../common');
30
30
 
31
- module.exports = function (core) {
31
+ module.exports = function(core) {
32
32
  const {
33
33
  depHooks,
34
34
  patcher,
@@ -54,7 +54,7 @@ module.exports = function (core) {
54
54
 
55
55
  const sequelize = (core.assess.dataflow.sinks.sequelize = {});
56
56
 
57
- sequelize.install = function () {
57
+ sequelize.install = function() {
58
58
  const sequelizeQueryPatchName = 'sequelize.prototype.query';
59
59
  depHooks.resolve({ name: 'sequelize' }, (sequelize) => {
60
60
  patcher.patch(sequelize.prototype, 'query', {
@@ -69,7 +69,7 @@ module.exports = function (core) {
69
69
 
70
70
  try {
71
71
  const queryInfo = tracker.getData(query);
72
- const isVulnerableQuery = isVulnerable(requiredTag, safeTags, queryInfo.tags);
72
+ const isVulnerableQuery = queryInfo && isVulnerable(requiredTag, safeTags, queryInfo.tags);
73
73
 
74
74
  if (queryInfo && !isVulnerableQuery && config.assess.safe_positives.enable) {
75
75
  reportSafePositive({
@@ -94,8 +94,8 @@ module.exports = function (core) {
94
94
  typeof args[0] === 'string' ? args[0] : inspect(args[0]);
95
95
  const inspectedOptions = args[1] ? inspect(args[1]) : '';
96
96
  const contextArgs = args[1]
97
- ? `${sqlValue}, ${inspectedOptions}`
98
- : sqlValue;
97
+ ? `'${sqlValue}', ${inspectedOptions}`
98
+ : `'${sqlValue}'`;
99
99
 
100
100
  const reportedArgs = [{ value: sqlValue, tracked: true }];
101
101
  args[1] &&
@@ -104,6 +104,8 @@ module.exports = function (core) {
104
104
  const event = createSinkEvent({
105
105
  context: `sequelize.prototype.query(${contextArgs})`,
106
106
  name: sequelizeQueryPatchName,
107
+ moduleName: 'sequelize',
108
+ methodName: 'prototype.query',
107
109
  history: [queryInfo],
108
110
  object: {
109
111
  value: 'sequelize.prototype',
@@ -29,7 +29,7 @@ const safeTags = [
29
29
  CUSTOM_ENCODED,
30
30
  ];
31
31
 
32
- module.exports = function (core) {
32
+ module.exports = function(core) {
33
33
  const {
34
34
  depHooks,
35
35
  patcher,
@@ -43,7 +43,7 @@ module.exports = function (core) {
43
43
  },
44
44
  } = core;
45
45
 
46
- const pre = (name) => (data) => {
46
+ const pre = (name, method) => (data) => {
47
47
  const store = sources.getStore()?.assess;
48
48
  if (
49
49
  !store ||
@@ -59,6 +59,9 @@ module.exports = function (core) {
59
59
 
60
60
  const event = createSinkEvent({
61
61
  name,
62
+ moduleName: 'sqlite3',
63
+ methodName: `Database.prototype.${method}`,
64
+ context: `db.${method}('${strInfo.value}')`,
62
65
  history: [strInfo],
63
66
  object: {
64
67
  value: '[Module<sqlite3>].Database',
@@ -74,6 +77,7 @@ module.exports = function (core) {
74
77
  source: 'P0',
75
78
  stacktraceOpts: {
76
79
  contructorOpt: data.hooked,
80
+ prependFrames: [data.orig]
77
81
  },
78
82
  });
79
83
 
@@ -93,7 +97,7 @@ module.exports = function (core) {
93
97
  patcher.patch(sqlite3.Database.prototype, method, {
94
98
  name,
95
99
  patchType,
96
- pre: pre(name)
100
+ pre: pre(name, method)
97
101
  });
98
102
  });
99
103
  });
@@ -19,6 +19,7 @@ const {
19
19
  InputType,
20
20
  DataflowTag,
21
21
  isString,
22
+ join,
22
23
  } = require('@contrast/common');
23
24
 
24
25
  module.exports = function(core) {
@@ -115,7 +116,7 @@ module.exports = function(core) {
115
116
  }
116
117
 
117
118
  traverse(_data, (path, fieldName, value, obj) => {
118
- const pathName = path.join('.');
119
+ const pathName = join(path, '.');
119
120
 
120
121
  if (sourceContext.sourceEventsCount >= max) {
121
122
  core.logger.trace({ inputType, name }, 'exiting assess source handling - %s max events exceeded', max);
@@ -80,7 +80,7 @@ module.exports = function(core) {
80
80
  pre(data) {
81
81
  const [name = '', value] = data.args;
82
82
  if (toLowerCase(name) === 'content-type' && value) {
83
- store.assess.responseData.contentType = value;
83
+ scopes.sources.getStore().assess.responseData.contentType = value;
84
84
  }
85
85
  }
86
86
  });
@@ -134,6 +134,29 @@ function createAppendTags(firstTags, secondTags, offset) {
134
134
  return Object.keys(ret).length ? ret : null;
135
135
  }
136
136
 
137
+ function createOverlappingTags(tags, startIndex, endIndex) {
138
+ const overlappingTags = {};
139
+
140
+ Object.entries(tags).forEach(([tag, tagRanges]) => {
141
+ const overlappingRanges = [];
142
+
143
+ for (let i = 0; i < tagRanges.length; i += 2) {
144
+ const start = tagRanges[i];
145
+ const end = tagRanges[i + 1];
146
+
147
+ if (end >= startIndex && start <= endIndex) {
148
+ overlappingRanges.push([start, end]);
149
+ }
150
+ }
151
+
152
+ if (overlappingRanges.length > 0) {
153
+ overlappingTags[tag] = overlappingRanges;
154
+ }
155
+ });
156
+
157
+ return overlappingTags;
158
+ }
159
+
137
160
  /**
138
161
  * assumes:
139
162
  * - no mutation of arguments
@@ -185,5 +208,6 @@ module.exports = {
185
208
  createSubsetTags,
186
209
  createAppendTags,
187
210
  createFullLengthCopyTags,
188
- createMergedTags
211
+ createMergedTags,
212
+ createOverlappingTags
189
213
  };
@@ -34,7 +34,8 @@ module.exports = function tracker(core) {
34
34
 
35
35
  function getData(value) {
36
36
  if (typeof value === 'string') {
37
- return distringuish.getProperties(value);
37
+ const props = distringuish.getProperties(value);
38
+ return props?.untracked ? null : props;
38
39
  }
39
40
 
40
41
  return objMap.get(value) || null;
@@ -126,11 +127,10 @@ module.exports = function tracker(core) {
126
127
  if (typeof value === 'string') {
127
128
  const props = distringuish.getProperties(value);
128
129
  if (props) {
129
- Object.assign(props, {
130
- history: [],
131
- tags: {},
132
- });
133
- delete props.resultTracked;
130
+ for (const key of Object.keys(props)) {
131
+ delete props[key];
132
+ }
133
+ props.untracked = true;
134
134
  }
135
135
  return distringuish.internalize(value);
136
136
  }
package/lib/index.js CHANGED
@@ -16,6 +16,7 @@
16
16
  'use strict';
17
17
 
18
18
  const { callChildComponentMethodsSync } = require('@contrast/common');
19
+ const sessionConfiguration = require('./session-configuration');
19
20
  const dataflow = require('./dataflow');
20
21
  const responseScanning = require('./response-scanning');
21
22
 
@@ -26,6 +27,7 @@ module.exports = function assess(core) {
26
27
 
27
28
  // Does this order matter? Probably not
28
29
  // 1. dataflow
30
+ sessionConfiguration(core);
29
31
  dataflow(core);
30
32
  responseScanning(core);
31
33
 
@@ -15,7 +15,7 @@
15
15
 
16
16
  'use strict';
17
17
 
18
- const { join, substring, toLowerCase, split, trim } = require('@contrast/common');
18
+ const { join, substring, toLowerCase, split, trim, replace } = require('@contrast/common');
19
19
 
20
20
  //
21
21
  // General HTML utils
@@ -32,7 +32,7 @@ const reHasUnescapedHtml = RegExp(reUnescapedHtml.source);
32
32
 
33
33
  function escapeHtml(string) {
34
34
  return (string && reHasUnescapedHtml.test(string))
35
- ? string.replace(reUnescapedHtml, (chr) => htmlEscapes[chr])
35
+ ? replace(string, reUnescapedHtml, (chr) => htmlEscapes[chr])
36
36
  : (string || '');
37
37
  }
38
38
 
@@ -0,0 +1,34 @@
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, Event } = require('@contrast/common');
19
+
20
+ module.exports = function(core) {
21
+ const { messages } = core;
22
+ const sessionConfiguration = core.assess.sessionConfiguration = {
23
+ reportFindings(_sourceContext, vulnerabilityMetadata) {
24
+ messages.emit(Event.ASSESS_SESSION_CONFIGURATION_FINDING, vulnerabilityMetadata);
25
+ },
26
+ };
27
+ require('./install/http')(core);
28
+
29
+ sessionConfiguration.install = function() {
30
+ callChildComponentMethodsSync(sessionConfiguration, 'install');
31
+ };
32
+
33
+ return sessionConfiguration;
34
+ };
@@ -0,0 +1,79 @@
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 { SessionConfigurationRule, split, toLowerCase } = require('@contrast/common');
19
+
20
+ module.exports = function(core) {
21
+ const {
22
+ depHooks,
23
+ patcher,
24
+ scopes: { sources },
25
+ assess: {
26
+ sessionConfiguration: {
27
+ reportFindings
28
+ }
29
+ }
30
+ } = core;
31
+ const http = core.assess.sessionConfiguration.httpInstrumentation = {};
32
+
33
+ const patchType = 'session-configuration';
34
+
35
+ http.install = function() {
36
+ [
37
+ { name: 'http', responseObj: 'ServerResponse' },
38
+ { name: 'https', responseObj: 'ServerResponse' },
39
+ { name: 'http2', responseObj: 'Http2ServerResponse' }
40
+ ].forEach(({ name, responseObj }) => {
41
+ depHooks.resolve({ name }, (module) => {
42
+ patcher.patch(module[responseObj].prototype, 'setHeader', {
43
+ name: `${name}.${responseObj}.prototype.setHeader`,
44
+ patchType,
45
+ post(data) {
46
+ const sourceContext = sources.getStore()?.assess;
47
+ if (!sourceContext) return;
48
+
49
+ const [key, val] = data.args;
50
+ if (key === 'Set-Cookie') {
51
+ const [cookies] = val;
52
+ const parsedCookies = split(toLowerCase(cookies), '; ');
53
+
54
+ if (!parsedCookies.includes('httponly')) {
55
+ reportFindings(sourceContext, {
56
+ ruleId: SessionConfigurationRule.HTTPONLY,
57
+ props: {
58
+ evidence: cookies
59
+ }
60
+ });
61
+ }
62
+
63
+ if (!parsedCookies.includes('secure')) {
64
+ reportFindings(sourceContext, {
65
+ ruleId: SessionConfigurationRule.SECURE_FLAG_MISSING,
66
+ props: {
67
+ evidence: cookies
68
+ }
69
+ });
70
+ }
71
+ }
72
+ }
73
+ });
74
+ });
75
+ });
76
+ };
77
+
78
+ return http;
79
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrast/assess",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
4
4
  "description": "",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -15,7 +15,7 @@
15
15
  "dependencies": {
16
16
  "@contrast/distringuish": "^4.1.0",
17
17
  "@contrast/scopes": "1.4.0",
18
- "@contrast/common": "1.11.0",
18
+ "@contrast/common": "1.12.0",
19
19
  "parseurl": "^1.3.3"
20
20
  }
21
21
  }