@contrast/agent 4.8.0 → 4.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.
@@ -0,0 +1,307 @@
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 logger = require('../../core/logger')('contrast:assess.sources');
18
+ const { PATCH_TYPES } = require('../../constants');
19
+ const { CallContext, SourceEvent } = require('../models');
20
+ const TraceEventSource = require('../../reporter/models/trace-event-source');
21
+ const { AsyncStorage, KEYS } = require('../../core/async-storage');
22
+ const TagRange = require('../models/tag-range');
23
+ const patcher = require('../../hooks/patcher');
24
+ const tracker = require('../../tracker');
25
+ const parseurl = require('parseurl');
26
+
27
+ const SOURCE_EVENT_MAX = 250;
28
+ const trackedObjects = new WeakMap();
29
+
30
+ class SourceEventHandler {
31
+ constructor({
32
+ config = {
33
+ agent: {
34
+ node: {
35
+ array_request_sampling: {
36
+ enable: true,
37
+ threshold: 50,
38
+ interval: 5
39
+ }
40
+ }
41
+ }
42
+ },
43
+ ensureReq = true,
44
+ exclusions,
45
+ stackOpts,
46
+ snapshot,
47
+ signature,
48
+ type = 'UNKNOWN'
49
+ } = {}) {
50
+ this.config = config;
51
+ this.exclusions = exclusions;
52
+ this.type = type;
53
+ this.snapshot = snapshot;
54
+ this.signature = signature;
55
+ this.stackOpts = stackOpts;
56
+ this.ensureReq = ensureReq;
57
+ this.samplingEnabled = config.agent.node.array_request_sampling.enable;
58
+ this.samplingThreshold = config.agent.node.array_request_sampling.threshold;
59
+ this.samplingInterval = this.samplingEnabled
60
+ ? config.agent.node.array_request_sampling.interval
61
+ : 1;
62
+
63
+ this.inputExclusions = [];
64
+ this.urlExclusions = [];
65
+ this.skipEvent = false;
66
+ this.sourceEventCount = 0;
67
+
68
+ this.init();
69
+ }
70
+
71
+ init() {
72
+ // values reused in functions
73
+ this.req = AsyncStorage.get(KEYS.REQ);
74
+
75
+ // NODE-1431: prevents crashing when req is not present in AsyncStorage.
76
+ if (!this.req) {
77
+ if (!this.ensureReq) return;
78
+
79
+ this.skipEvent = true;
80
+ logger.debug(
81
+ 'failed to handle source event for type: %s; request not present in async storage',
82
+ this.type
83
+ );
84
+ }
85
+
86
+ if (!this.exclusions) return;
87
+
88
+ const { pathname } = parseurl(this.req);
89
+
90
+ this.inputExclusions = this.exclusions
91
+ .getInputExclusions('assess')
92
+ .reduce((acc, e) => {
93
+ if (!e.matchesUrl(pathname)) return acc;
94
+
95
+ // `isNamed` is false for exclusion types such as BODY and QUERYSTRING which
96
+ // apply to the entire set of params not only those by a specified name. Also
97
+ // is true if the input name is set to wildcard "*" meaning it should apply to
98
+ // all values. In each case there's no need to wrap if all rules are excluded.
99
+ if (!this.skipEvent && e.appliesToAllAssessRules() && !e.isNamed) {
100
+ logExclusionMessage(e, this.type);
101
+ this.skipEvent = true;
102
+ }
103
+
104
+ acc.push(e);
105
+ return acc;
106
+ }, []);
107
+
108
+ this.urlExclusions = this.exclusions
109
+ .getUrlExclusions('assess')
110
+ .reduce((acc, e) => {
111
+ if (!e.matchesUrl(pathname)) return acc;
112
+
113
+ if (!this.skipEvent && e.appliesToAllAssessRules()) {
114
+ logExclusionMessage(e, this.type);
115
+ this.skipEvent = true;
116
+ }
117
+
118
+ acc.push(e);
119
+ return acc;
120
+ }, []);
121
+ }
122
+
123
+ /**
124
+ *
125
+ * @param {object} param
126
+ * @param {object} param.obj usually the incoming message
127
+ * @param {string} param.prop obj[prop] is containing user-controlled data
128
+ */
129
+ handle({ obj, prop }) {
130
+ this.typeKey = prop;
131
+
132
+ if (this.skipEvent) return;
133
+
134
+ this.traverseAndTrack({ obj, key: prop });
135
+ }
136
+
137
+ // eslint-disable-next-line complexity
138
+ traverseAndTrack({ obj, key, path = [] }) {
139
+ const value = obj[key];
140
+
141
+ if (!value) return;
142
+
143
+ if (this.sourceEventCount > SOURCE_EVENT_MAX) {
144
+ logger.debug('max sources exceeded for %s', this.type);
145
+ return;
146
+ }
147
+
148
+ if (Array.isArray(value)) {
149
+ const limit = Math.min(value.length, this.samplingThreshold);
150
+
151
+ trackedObjects.set(value, { ...this, path, key });
152
+
153
+ for (let i = 0; i < limit; i += this.samplingInterval) {
154
+ this.traverseAndTrack({
155
+ obj: value,
156
+ key: i,
157
+ path: [...path, i]
158
+ });
159
+ }
160
+ } else if (Buffer.isBuffer(value)) {
161
+ this.sourceEventCount++;
162
+ patcher.patch(value, 'toString', {
163
+ name: 'Buffer.toString',
164
+ alwaysRun: true,
165
+ patchType: PATCH_TYPES.MISC,
166
+ post: (wrapCtx) => {
167
+ this.trackStringProp({
168
+ obj: wrapCtx,
169
+ key: 'result',
170
+ path
171
+ });
172
+ }
173
+ });
174
+ } else if (typeof value === 'string' || value instanceof String) {
175
+ this.sourceEventCount++;
176
+ this.trackStringProp({ obj, key, path });
177
+ } else if (typeof value === 'object') {
178
+ trackedObjects.set(value, { ...this, path, key });
179
+
180
+ for (const objKey of Object.keys(value)) {
181
+ this.traverseAndTrack({
182
+ obj: value,
183
+ path: [...path, objKey],
184
+ key: objKey
185
+ });
186
+ }
187
+ }
188
+ }
189
+
190
+ trackStringProp({ obj, key, path, value = obj[key] }) {
191
+ const trackData = tracker.track(value);
192
+ if (!trackData) {
193
+ return;
194
+ }
195
+
196
+ const name = path.join('.');
197
+ const { props, str: result } = trackData;
198
+
199
+ props.tagRanges = this.getTagRanges({ name, result });
200
+ props.event = new SourceEvent({
201
+ context: new CallContext({
202
+ obj: 'IncomingMessage {}',
203
+ args: [`${this.typeKey}.${name}`],
204
+ result,
205
+ stackOpts: this.stackOpts
206
+ }),
207
+ code: `req.${this.typeKey}.${name}`,
208
+ signature: this.signature,
209
+ tagRanges: props.tagRanges,
210
+ target: 'R',
211
+ type: this.type,
212
+ name
213
+ });
214
+
215
+ // NOTE: this used to happen on access in SourceMembrane.onString(), though we
216
+ // should be able to determine access of source value during dataflow - when a
217
+ // source is tracked into PROPAGATION or SINK event.
218
+ storeSource({ type: this.type, name: key });
219
+
220
+ obj[key] = result;
221
+ }
222
+
223
+ getTagRanges({ result, name }) {
224
+ const stop = result.length - 1;
225
+ const tagRanges = [new TagRange(0, stop, 'untrusted')];
226
+
227
+ const typeLowerCase = this.type.toLowerCase();
228
+ const nameLowerCase = name.toLocaleLowerCase();
229
+
230
+ if (typeLowerCase === 'header' && nameLowerCase !== 'referer') {
231
+ tagRanges.push(new TagRange(0, stop, 'header'));
232
+ }
233
+
234
+ if (typeLowerCase === 'cookie') {
235
+ tagRanges.push(new TagRange(0, stop, 'cookie'));
236
+ }
237
+
238
+ if (!this.inputExclusions.length) {
239
+ return tagRanges;
240
+ }
241
+
242
+ const rules = new Set();
243
+
244
+ /*
245
+ Maybe it's pointless because we set skipEvent to true if appliesToAllAssessRules
246
+ and we won't get here
247
+ */
248
+ const exclusions = this.inputExclusions.filter(
249
+ (e) => !e.appliesToAllAssessRules()
250
+ );
251
+
252
+ for (const exclusion of exclusions) {
253
+ if (!exclusion.matches(name)) {
254
+ continue;
255
+ }
256
+
257
+ for (const ruleId of exclusion.assessmentRulesList) {
258
+ logger.debug(
259
+ 'excluding %s %s value from %s (%s)',
260
+ this.type,
261
+ name,
262
+ ruleId,
263
+ exclusion.name
264
+ );
265
+
266
+ if (!rules.has(ruleId)) {
267
+ tagRanges.push(new TagRange(0, stop, `exclusion:${ruleId}`));
268
+ }
269
+
270
+ rules.add(ruleId);
271
+ }
272
+ }
273
+
274
+ return tagRanges;
275
+ }
276
+ }
277
+
278
+ function storeSource({ type, name }) {
279
+ let storedSources = AsyncStorage.get(KEYS.SOURCES);
280
+
281
+ if (!storedSources) {
282
+ // if this is the first source stored on the request, initialize a map
283
+ // and set the storedSources to be a reference to the new, empty map.
284
+ storedSources = new Map();
285
+ AsyncStorage.set(KEYS.SOURCES, storedSources);
286
+ }
287
+
288
+ // there are cases where AsyncStorage context is out of sync, add defensive code so we don't
289
+ // crash.
290
+ if (storedSources) {
291
+ // save event for route coverage (data pulled in for RouteInfo object)
292
+ // only want to save unique values
293
+ storedSources.set(`${type}-${name}`, new TraceEventSource({ type, name }));
294
+ }
295
+ }
296
+
297
+ function logExclusionMessage(exclusion, type) {
298
+ const { name } = exclusion;
299
+ logger.debug(
300
+ 'excluding %s inputs from all assess rules (%s)',
301
+ type.toLowerCase(),
302
+ name
303
+ );
304
+ }
305
+
306
+ module.exports.SourceEventHandler = SourceEventHandler;
307
+ module.exports.trackedObjects = trackedObjects;
@@ -26,9 +26,12 @@ const parseurl = require('parseurl');
26
26
  const {
27
27
  EXCLUSION_INPUT_TYPES: { BODY, HEADER, PARAMETER, QUERYSTRING, COOKIE }
28
28
  } = require('../../constants');
29
+ const { Signature } = require('../models');
29
30
 
30
31
  const sources = module.exports;
31
32
 
33
+ const { SourceEventHandler } = require('./event-handler');
34
+
32
35
  /**
33
36
  * Registers sources for assess instrumentation
34
37
  */
@@ -60,19 +63,104 @@ sources.track = function(type, parent, key, membrane) {
60
63
  */
61
64
  sources.registerListeners = function({ config, exclusions }) {
62
65
  agentEmitter.on('assess.body', (obj, prop) => {
63
- sources.handleSourceEvent(config, exclusions, BODY, obj, prop);
66
+ if (!config.agent.traverse_and_track) {
67
+ return sources.handleSourceEvent(config, exclusions, BODY, obj, prop);
68
+ }
69
+
70
+ agentEmitter.emit('assess.source', {
71
+ config,
72
+ exclusions,
73
+ obj,
74
+ prop,
75
+ data: obj[prop],
76
+ type: BODY
77
+ });
64
78
  });
65
79
  agentEmitter.on('assess.headers', (obj, prop) => {
66
- sources.handleSourceEvent(config, exclusions, HEADER, obj, prop);
80
+ if (!config.agent.traverse_and_track) {
81
+ return sources.handleSourceEvent(config, exclusions, HEADER, obj, prop);
82
+ }
83
+
84
+ agentEmitter.emit('assess.source', {
85
+ obj,
86
+ prop,
87
+ data: obj[prop],
88
+ type: HEADER
89
+ });
67
90
  });
68
91
  agentEmitter.on('assess.params', (obj, prop) => {
69
- sources.handleSourceEvent(config, exclusions, PARAMETER, obj, prop);
92
+ if (!config.agent.traverse_and_track) {
93
+ return sources.handleSourceEvent(
94
+ config,
95
+ exclusions,
96
+ PARAMETER,
97
+ obj,
98
+ prop
99
+ );
100
+ }
101
+
102
+ agentEmitter.emit('assess.source', {
103
+ obj,
104
+ prop,
105
+ data: obj[prop],
106
+ type: PARAMETER
107
+ });
70
108
  });
71
109
  agentEmitter.on('assess.query', (obj, prop) => {
72
- sources.handleSourceEvent(config, exclusions, QUERYSTRING, obj, prop);
110
+ if (!config.agent.traverse_and_track) {
111
+ return sources.handleSourceEvent(
112
+ config,
113
+ exclusions,
114
+ QUERYSTRING,
115
+ obj,
116
+ prop
117
+ );
118
+ }
119
+
120
+ agentEmitter.emit('assess.source', {
121
+ obj,
122
+ prop,
123
+ data: obj[prop],
124
+ type: QUERYSTRING
125
+ });
73
126
  });
127
+
74
128
  agentEmitter.on('assess.cookies', (obj, prop) => {
75
- sources.handleSourceEvent(config, exclusions, COOKIE, obj, prop);
129
+ if (!config.agent.traverse_and_track) {
130
+ return sources.handleSourceEvent(config, exclusions, COOKIE, obj, prop);
131
+ }
132
+
133
+ agentEmitter.emit('assess.source', {
134
+ obj,
135
+ prop,
136
+ type: COOKIE
137
+ });
138
+ });
139
+
140
+ // might be helpful for clients to send add'l values in event arg
141
+ // - stackOpts: elide frames from function ref in client instrumentation
142
+ // - signature: rather than create shared one in the handler
143
+ // - or stack snapshot function - could share among SourceEvents
144
+ // - call context to share among SourceEvents
145
+ agentEmitter.on('assess.source', ({ obj, prop, type, signature }) => {
146
+ if (!signature) {
147
+ signature = new Signature({
148
+ moduleName: 'Object',
149
+ methodName: 'getter',
150
+ args: [prop],
151
+ return: 'String',
152
+ isModule: false
153
+ });
154
+ }
155
+
156
+ new SourceEventHandler({
157
+ config,
158
+ exclusions,
159
+ signature,
160
+ type,
161
+ stackOpts: undefined,
162
+ snapshot: undefined
163
+ }).handle({ obj, prop });
76
164
  });
77
165
  };
78
166
 
@@ -0,0 +1,23 @@
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 AssessSinks = require('./sinks');
18
+
19
+ module.exports = class SpdyInstrumentation {
20
+ constructor(agent) {
21
+ new AssessSinks(agent);
22
+ }
23
+ };
@@ -0,0 +1,23 @@
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 ReflectedXss = require('./xss');
18
+
19
+ module.exports = class SpdySinks {
20
+ constructor(agent) {
21
+ new ReflectedXss(agent);
22
+ }
23
+ };
@@ -0,0 +1,84 @@
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 agentEmitter = require('../../../agent-emitter');
18
+ const { HTTP_RESPONSE_HOOKED_METHOD_KEYS } = require('../../../constants');
19
+ const policy = require('../../policy');
20
+ const { Signature, CallContext } = require('../../models');
21
+
22
+ class SpdyXss {
23
+ constructor(agent) {
24
+ this.common = require('../../sinks/common')(agent);
25
+ this.rules = policy.rules;
26
+ this.ruleId = 'reflected-xss';
27
+ this.signature = new Signature({
28
+ moduleName: 'spdy.response',
29
+ methodName: 'push',
30
+ isModule: false
31
+ });
32
+ agentEmitter.on(
33
+ HTTP_RESPONSE_HOOKED_METHOD_KEYS.PUSH,
34
+ this.checkResult.bind(this)
35
+ );
36
+ }
37
+
38
+ /**
39
+ * checks if an assess rule is enabled in policy
40
+ */
41
+ get enabled() {
42
+ return (
43
+ this.rules &&
44
+ this.rules['reflected-xss'] &&
45
+ this.rules['reflected-xss'].enabled
46
+ );
47
+ }
48
+
49
+ checkResult(body) {
50
+ if (!this.enabled) {
51
+ return;
52
+ }
53
+
54
+ const { ruleId, signature } = this;
55
+
56
+ const {
57
+ isVulnerable,
58
+ xss: { disallowedTags },
59
+ requiredTags,
60
+ report
61
+ } = this.common;
62
+
63
+ if (
64
+ isVulnerable({
65
+ input: body,
66
+ disallowedTags,
67
+ requiredTags,
68
+ ruleId
69
+ })
70
+ ) {
71
+ const ctxt = new CallContext({
72
+ obj: body,
73
+ args: [body],
74
+ result: body,
75
+ stackOpts: {
76
+ constructorOpt: agentEmitter.emit
77
+ }
78
+ });
79
+ report({ ruleId, signature, input: body, ctxt });
80
+ }
81
+ }
82
+ }
83
+
84
+ module.exports = SpdyXss;
@@ -31,7 +31,8 @@ const technologies = {
31
31
  'fastify',
32
32
  'restify',
33
33
  'loopback',
34
- 'kraken-js'
34
+ 'kraken-js',
35
+ 'sails'
35
36
  ],
36
37
  templating: ['jade', 'ejs', 'nunjucks', 'mustache', 'dust', 'handlebars'],
37
38
  loggers: ['winston', 'debug'],
package/lib/constants.js CHANGED
@@ -644,7 +644,8 @@ const REQUIRED_SIGNATURE_KEYS = [
644
644
 
645
645
  const HTTP_RESPONSE_HOOKED_METHOD_KEYS = {
646
646
  WRITE_HEAD: Symbol('writeHead'),
647
- END: Symbol('end')
647
+ END: Symbol('end'),
648
+ PUSH: Symbol('push')
648
649
  };
649
650
 
650
651
  const PATCH_TYPES = {
package/lib/contrast.js CHANGED
@@ -178,7 +178,7 @@ contrastAgent.configureGlobalLogger = function(config, args, target = global) {
178
178
 
179
179
  function getAgentArgs(options) {
180
180
  const agentArgs = {};
181
- options.options.forEach((opt) => {
181
+ program.options.forEach((opt) => {
182
182
  if (opt.name() !== 'application.args' && options[opt.name()]) {
183
183
  agentArgs[opt.name()] = options[opt.name()];
184
184
  }
@@ -243,8 +243,8 @@ contrastAgent.prepare = function(...args) {
243
243
 
244
244
  logger.info('Using config file at %s', config.configFile);
245
245
  // log the argv before and after modification.
246
- logger.info(`Original argv: ${options.rawArgs.join(', ')}`);
247
- logger.info(`Modified argv: ${options.args.join(', ')}`);
246
+ logger.info(`Original argv: ${program.rawArgs.join(', ')}`);
247
+ logger.info(`Modified argv: ${program.args.join(', ')}`);
248
248
 
249
249
  agent.config = config;
250
250
  agent.tsFeatureSet.config = config;
@@ -335,12 +335,12 @@ contrastAgent.init = async function(args, isCli = false) {
335
335
  // source: args passed to cli, destination: args after cli parsed it
336
336
  .action(async function callPrepare(options, commanderArgs = []) {
337
337
  // the user app main differs if a runner vs preload
338
- script = isCli ? options.args[0] : options.rawArgs[1];
338
+ script = isCli ? program.args[0] : program.rawArgs[1];
339
339
  options.script = script;
340
340
  // need to slice off app main in runner mode
341
341
  options['application.args'] = isCli
342
- ? options.args.slice(1)
343
- : options.args;
342
+ ? program.args.slice(1)
343
+ : program.args;
344
344
 
345
345
  try {
346
346
  enabled = await contrastAgent.prepare(options, commanderArgs, isCli);
@@ -18,3 +18,4 @@ require('./sqlite3');
18
18
  require('./postgres');
19
19
  require('./dynamodb');
20
20
  require('./dynamodbv3');
21
+ require('./rethinkdb');
@@ -28,25 +28,29 @@ ModuleHook.resolve(
28
28
  patchType: PATCH_TYPES.ARCH_COMPONENT,
29
29
  alwaysRun: true,
30
30
  post(ctx) {
31
- try {
32
- const { servers = [] } = this.s.options;
33
- if (servers.length === 0) {
34
- logger.warn('Unable to find any MongoDB servers\n');
35
- }
36
- for (const server of servers) {
37
- agentEmitter.emit('architectureComponent', {
38
- vendor: 'MongoDB',
39
- url: `mongodb://${server.host}`,
40
- remoteHost: '',
41
- remotePort: server.port
42
- });
43
- }
44
- } catch (err) {
45
- logger.warn(
46
- 'unable to report MongoDB architecture component\n%o',
47
- err
48
- );
31
+ if (!ctx.result || !ctx.result.then) {
32
+ return;
49
33
  }
34
+
35
+ // We should report only when connection is successful
36
+ ctx.result.then(function(client) {
37
+ try {
38
+ const { servers = [] } = ctx.obj.s && ctx.obj.s.options;
39
+ for (const server of servers) {
40
+ agentEmitter.emit('architectureComponent', {
41
+ vendor: 'MongoDB',
42
+ url: `mongodb://${server.host}:${server.port}`,
43
+ remoteHost: '',
44
+ remotePort: server.port
45
+ });
46
+ }
47
+ } catch (err) {
48
+ logger.warn(
49
+ 'unable to report MongoDB architecture component\n%o',
50
+ err
51
+ );
52
+ }
53
+ });
50
54
  }
51
55
  });
52
56
  }