@contrast/agent 4.15.1 → 4.16.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/README.md CHANGED
@@ -108,4 +108,4 @@ api:
108
108
  | api.service_key | Contrast user account service key |
109
109
  | api.url | Address of the Contrast installation you would like your agent to report to |
110
110
 
111
- For detailed installation and configuration instructions, see the [Node.js Agent documentation](https://docs.contrastsecurity.com/installation-nodeconfig.html).
111
+ For detailed installation and configuration instructions, see the [Node.js Agent documentation](https://docs.contrastsecurity.com/en/install-node-js.html).
@@ -26,34 +26,38 @@ const disallowedTags = [
26
26
  'string-type-checked',
27
27
  ];
28
28
  const requiredTags = ['untrusted'];
29
- const moduleName = 'aws-sdk';
30
- const moduleNameV3 = '@aws-sdk/client-dynamodb';
31
- const relevantKeys = {
32
- v2: ['ExpressionAttributeValues', 'ExclusiveStartKey', 'ScanFilter'],
33
- v3: [
34
- 'ComparisonOperator',
35
- 'FilterExpression',
36
- 'ProjectionExpression',
37
- 'ScanFilter',
38
- ],
39
- };
40
- const requests = new WeakSet();
41
29
 
42
- // map data types to methods for extracting
43
- // values
44
- const dataTypes = {
45
- S: 'value',
46
- N: 'value',
47
- B: 'value',
48
- SS: 'array',
49
- NS: 'array',
50
- BS: 'array',
51
- M: 'object',
52
- L: 'collection',
53
- NULL: 'value',
54
- BOOL: 'value',
30
+ /*
31
+ * Schema of attributes the Node Agent is looking for user-controlled data
32
+ * The only exception to the rule so far is the ScanFilter in ScanCommand.
33
+ * It has nested schema too. So far we are only reporting a NoSQL Injection
34
+ * if ComparisonOperator within the ScanFilter is user-controlled.
35
+ * ScanFilter is handled individually in extractValues function
36
+ * */
37
+ const trackSchemaCommands = {
38
+ 'scan': {
39
+ attributes: [
40
+ 'ExpressionAttributeValues',
41
+ 'ExclusiveStartKey',
42
+ 'ScanFilter'
43
+ ]
44
+ },
45
+ 'executeStatement': { attributes: ['Statement'] },
46
+ 'ScanCommand': {
47
+ attributes: [
48
+ 'ComparisonOperator',
49
+ 'FilterExpression',
50
+ 'ProjectionExpression',
51
+ 'ScanFilter',
52
+ ]
53
+ },
54
+ 'ExecuteStatementCommand': {
55
+ attributes: ['Statement']
56
+ }
55
57
  };
56
58
 
59
+ const requests = new WeakSet();
60
+
57
61
  /**
58
62
  * Extracts all values from either a dynamo document client or client
59
63
  *
@@ -62,44 +66,25 @@ const dataTypes = {
62
66
  * @param {string} version DynamoDB SDK version
63
67
  * @return {Array} all string values from payload
64
68
  */
65
- function extractValues(payload = {}, mode = null, version = 'v2') {
69
+ function extractValues(command, payload = {}) {
66
70
  return _.flatten(
67
- relevantKeys[version].map((key) => {
68
- if (payload[key] === undefined) return;
71
+ trackSchemaCommands[command].attributes.map((key) => {
72
+ if (payload[key] == undefined) return;
69
73
  if (typeof payload[key] === 'string') return payload[key];
74
+
75
+ // ScanFilter is an exception. It is almost safe for any nested attribute
76
+ if (key === 'ScanFilter') {
77
+ // collect the values from ComparisonOperator attributes ONLY
78
+ return getComparisonValues(payload[key]);
79
+ }
80
+
70
81
  const values = _.values(payload[key]);
71
- const extractionMethod =
72
- mode === 'client' ? findTypedValues.bind(this) : findValues.bind(this);
82
+ const extractionMethod = findValues.bind(this);
73
83
  return _.flattenDeep(extractionMethod(values));
74
84
  }),
75
85
  );
76
86
  }
77
87
 
78
- /**
79
- * Extracts all strings from a dynamo client payload
80
- * Note: Each key is typed so we need to properly extract keys based on AttributeValue types
81
- *
82
- * @param {Object} values for all keys in ExpressionAttributeValues, ExclusiveStartKey or ScanFilter
83
- * @return {Array} all values
84
- */
85
- function findTypedValues(values) {
86
- return _.map(values, (value) => {
87
- const [type] = Object.keys(value);
88
- switch (dataTypes[type]) {
89
- case 'value':
90
- return value[type];
91
- case 'array':
92
- return _.values(value[type]);
93
- case 'object': {
94
- const values = _.values(value[type]);
95
- return findTypedValues(values);
96
- }
97
- case 'collection':
98
- return findTypedValues(value[type]);
99
- }
100
- });
101
- }
102
-
103
88
  /**
104
89
  * We only track strings. some data types
105
90
  * can contain strings as keys or values but
@@ -139,6 +124,27 @@ function findValues(values) {
139
124
  });
140
125
  }
141
126
 
127
+ /**
128
+ * Check if key exists in ScanFilter bject and return the value if so
129
+ * Each element of ScanFilter is an object with predictable structure
130
+ * "Genre": {
131
+ * "AttributeValueList":[ {"S":"Rock"} ],
132
+ * "ComparisonOperator": "EQ"
133
+ * }
134
+ *
135
+ * @param {Object} values for all keys in a given payload
136
+ * @param {String} key name we are looking the value of
137
+ * @return {Any} if value is found for a given key
138
+ */
139
+ const getComparisonValues = (obj) => Object.keys(obj).map(field => {
140
+ if (typeof obj[field] === 'object' && Object.prototype.hasOwnProperty.call(
141
+ obj[field],
142
+ 'ComparisonOperator'
143
+ )) {
144
+ return obj[field].ComparisonOperator;
145
+ }
146
+ });
147
+
142
148
  module.exports = ({ common }) => {
143
149
  const { isVulnerable, report } = common;
144
150
 
@@ -147,11 +153,11 @@ module.exports = ({ common }) => {
147
153
  * Registers the hooks for client and document client scan
148
154
  */
149
155
  dynamoSink.handle = function () {
150
- moduleHook.resolve({ name: moduleName }, (aws) => {
156
+ moduleHook.resolve({ name: 'aws-sdk' }, (aws) => {
151
157
  const client = aws.DynamoDB.prototype;
152
158
 
153
159
  patcher.patch(client, 'makeRequest', {
154
- name: `${moduleName}.DynamoDB.prototype`,
160
+ name: 'aws-sdk.DynamoDB.prototype',
155
161
  patchType: PATCH_TYPES.ASSESS_SINK,
156
162
  alwaysRun: true,
157
163
  post(data) {
@@ -163,7 +169,8 @@ module.exports = ({ common }) => {
163
169
  if (!AsyncStorage.getContext()) {
164
170
  return;
165
171
  }
166
- if (data.args[0] === 'scan') {
172
+
173
+ if (Object.keys(trackSchemaCommands).includes(data.args[0]) && data.args[1]) {
167
174
  if (requests.has(data.args[1])) {
168
175
  return;
169
176
  }
@@ -176,10 +183,10 @@ module.exports = ({ common }) => {
176
183
  result: data.result,
177
184
  };
178
185
 
179
- const values = extractValues(data.args[1], 'client', 'v2');
186
+ const values = extractValues(data.args[0], data.args[1]);
180
187
  dynamoSink.check({
181
188
  values,
182
- methodName: 'DynamoDB.prototype.scan',
189
+ methodName: `DynamoDB.prototype.${data.args[0]}`,
183
190
  data: ctxtData,
184
191
  });
185
192
  }
@@ -198,7 +205,7 @@ module.exports = ({ common }) => {
198
205
  name: 'aws-sdk.DynamoDB.DocumentClient.prototype',
199
206
  patchType: PATCH_TYPES.ASSESS_SINK,
200
207
  pre(data) {
201
- if (data.args[0] === 'scan') {
208
+ if (data.args[0] === 'scan' && data.args[1]) {
202
209
  requests.add(data.args[1]);
203
210
  }
204
211
  },
@@ -208,7 +215,7 @@ module.exports = ({ common }) => {
208
215
  name: 'aws-sdk.DynamoDB.DocumentClient.prototype',
209
216
  patchType: PATCH_TYPES.ASSESS_SINK,
210
217
  post(data) {
211
- const values = extractValues(data.args[0], 'docClient', 'v2');
218
+ const values = extractValues('scan', data.args[0]);
212
219
  dynamoSink.check({
213
220
  values,
214
221
  methodName: 'DynamoDB.DocumentClient.prototype.scan',
@@ -219,27 +226,31 @@ module.exports = ({ common }) => {
219
226
  }
220
227
  });
221
228
 
222
- moduleHook.resolve({ name: moduleNameV3 }, (aws) => {
229
+ moduleHook.resolve({ name: '@aws-sdk/client-dynamodb' }, (aws) => {
223
230
  const client = aws.DynamoDBClient.prototype;
224
231
 
225
232
  patcher.patch(client, 'send', {
226
- name: `${moduleNameV3}.ScanCommand.prototype`,
233
+ name: '@aws-sdk/client-dynamodb.ScanCommand.prototype',
227
234
  patchType: PATCH_TYPES.ASSESS_SINK,
228
235
  alwaysRun: true,
229
236
  post(data) {
230
237
  if (!AsyncStorage.getContext()) return;
231
238
 
232
- if (data.args[0] instanceof aws.ScanCommand) {
233
- const values = extractValues(
234
- data.args[0].input,
235
- 'docClient',
236
- 'v3',
237
- ).filter(Boolean);
238
- dynamoSink.check({
239
- data,
240
- values,
241
- methodName: 'DynamoDBClient.ScanCommand',
242
- });
239
+ if (data.args[0] && data.args[0].constructor && data.args[0].input) {
240
+ const sendCommand = data.args[0].constructor.name;
241
+
242
+ if (Object.keys(trackSchemaCommands).includes(sendCommand)) {
243
+ const values = extractValues(
244
+ sendCommand,
245
+ data.args[0].input
246
+ ).filter(Boolean);
247
+
248
+ dynamoSink.check({
249
+ data,
250
+ values,
251
+ methodName: `DynamoDBClient.${sendCommand}`,
252
+ });
253
+ }
243
254
  }
244
255
  },
245
256
  });
@@ -257,7 +268,7 @@ module.exports = ({ common }) => {
257
268
 
258
269
  if (vulnerableString.length) {
259
270
  const ctxt = new CallContext(data);
260
- const signature = new Signature({ moduleName, methodName });
271
+ const signature = new Signature({ moduleName: 'aws-sdk', methodName });
261
272
  report({ ruleId, signature, input: vulnerableString[0], ctxt });
262
273
  }
263
274
  };
@@ -57,7 +57,7 @@ module.exports.install = function() {
57
57
  handledErrors.add(error);
58
58
  } else {
59
59
  console.warn(
60
- `An Unhandled Rejection has been caught by the Contrast Security node-agent instrumentation. Error: ${error}`,
60
+ 'An Unhandled Rejection has been found in the instrumented code:\n%s', error
61
61
  );
62
62
  }
63
63
  }
@@ -1133,7 +1133,7 @@ class ProtectService {
1133
1133
  * @param {Rule[]} rules Rules from which to build findings
1134
1134
  * @returns {Object[]} The findings from the rules
1135
1135
  */
1136
- createFindings(rules, samples) {
1136
+ createFindings(rules = [], samples) {
1137
1137
  const findings = [];
1138
1138
  const speedracer = this.reporter.speedracer &&
1139
1139
  this.config.agent.node.speedracer_input_analysis;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrast/agent",
3
- "version": "4.15.1",
3
+ "version": "4.16.0",
4
4
  "description": "Node.js security instrumentation by Contrast Security",
5
5
  "keywords": [
6
6
  "security",
@@ -69,7 +69,7 @@
69
69
  "repository": {
70
70
  "type": "git"
71
71
  },
72
- "homepage": "https://docs.contrastsecurity.com/installation-node.html#node-overview",
72
+ "homepage": "https://docs.contrastsecurity.com/en/install-node-js.html",
73
73
  "dependencies": {
74
74
  "@babel/generator": "^7.12.1",
75
75
  "@babel/parser": "^7.12.3",