@contrast/agent 4.24.2 → 4.25.1
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/bin/{contrast-service-win32-x64 → contrast-service-win32-x64.exe} +0 -0
- package/lib/assess/express/sources.js +5 -4
- package/lib/assess/membrane/source-membrane.js +1 -1
- package/lib/assess/models/call-context.js +4 -13
- package/lib/assess/sources/index.js +202 -243
- package/lib/core/express/index.js +4 -2
- package/lib/hooks/encoding.js +2 -2
- package/lib/list-installed.js +1 -0
- package/lib/protect/express/sources.js +1 -1
- package/lib/reporter/speedracer/index.js +5 -1
- package/lib/tracker.js +85 -136
- package/package.json +3 -3
|
File without changes
|
|
@@ -15,21 +15,22 @@ Copyright: 2022 Contrast Security, Inc
|
|
|
15
15
|
'use strict';
|
|
16
16
|
|
|
17
17
|
const agentEmitter = require('../../agent-emitter');
|
|
18
|
-
const
|
|
18
|
+
const { EVENTS } = require('../../core/express/utils');
|
|
19
19
|
const {
|
|
20
|
-
prototype: { decorateRequest }
|
|
20
|
+
prototype: { decorateRequest },
|
|
21
21
|
} = require('../../hooks/frameworks/base');
|
|
22
|
-
const { EVENTS } = Helpers;
|
|
23
22
|
|
|
24
23
|
class ExpressSources {
|
|
25
24
|
constructor() {
|
|
26
25
|
agentEmitter.on(EVENTS.REQUEST_READY, ExpressSources.handleReady);
|
|
27
26
|
agentEmitter.on(EVENTS.BODY_PARSED, ExpressSources.handleBody);
|
|
28
27
|
agentEmitter.on(EVENTS.COOKIES_PARSED, ExpressSources.handleCookies);
|
|
29
|
-
agentEmitter.on(EVENTS.
|
|
28
|
+
agentEmitter.on(EVENTS.PARAMS_PARSED, ExpressSources.handleParams);
|
|
30
29
|
}
|
|
31
30
|
|
|
32
31
|
static handleReady(req, res, inputType) {
|
|
32
|
+
agentEmitter.emit('assess.url', req, '_parsedUrl');
|
|
33
|
+
req.originalUrl !== '/' && agentEmitter.emit('assess.url', req, 'originalUrl');
|
|
33
34
|
agentEmitter.emit('assess.query', req, 'query');
|
|
34
35
|
agentEmitter.emit('assess.headers', req, 'headers');
|
|
35
36
|
}
|
|
@@ -133,7 +133,7 @@ module.exports = class SourceMembrane extends Membrane {
|
|
|
133
133
|
* @return {string} returns tracked string with proper contrastProps
|
|
134
134
|
*/
|
|
135
135
|
onString(str, metadata) {
|
|
136
|
-
if (!this.ensureMetadata(metadata)) {
|
|
136
|
+
if (!this.ensureMetadata(metadata) && !metadata.rootLevelString) {
|
|
137
137
|
return str;
|
|
138
138
|
}
|
|
139
139
|
const tracked = tracker.track(str);
|
|
@@ -18,9 +18,9 @@ const util = require('util');
|
|
|
18
18
|
const CleanStack = require('../../util/clean-stack');
|
|
19
19
|
const tracker = require('../../tracker');
|
|
20
20
|
const stackFactory = require('../../core/stacktrace').singleton;
|
|
21
|
-
const distringuish = require('@contrast/distringuish-prebuilt');
|
|
22
21
|
const { PROXY_TARGET } = require('../../../lib/constants');
|
|
23
22
|
const TagRange = require('../models/tag-range');
|
|
23
|
+
const { toUntrackedString } = require('../utils');
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* Holds information about the call context of a function
|
|
@@ -116,12 +116,13 @@ module.exports = class CallContext {
|
|
|
116
116
|
}
|
|
117
117
|
if (arg[key] && typeof arg[key] === 'object' && iteration < 100) {
|
|
118
118
|
return CallContext.hasTrackedArg(arg[key], iteration += 1);
|
|
119
|
-
}
|
|
119
|
+
}
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
return false;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
// eslint-disable-next-line complexity
|
|
125
126
|
static getDisplayRange(arg, orgArg = arg, iteration = 0) {
|
|
126
127
|
if (tracker.getData(arg)) {
|
|
127
128
|
return new TagRange(0, arg.length - 1, 'untrusted');
|
|
@@ -188,17 +189,7 @@ module.exports = class CallContext {
|
|
|
188
189
|
*/
|
|
189
190
|
static valueString(value) {
|
|
190
191
|
if (_.isString(value)) {
|
|
191
|
-
|
|
192
|
-
// external strings to something that is referenced by its'
|
|
193
|
-
// own properties. For source strings we were creating
|
|
194
|
-
// a CallContext with the string, adding it to the SourceEvent,
|
|
195
|
-
// and then that SourceEvent was added to the external strings
|
|
196
|
-
// props. So there was a circular ref between the string
|
|
197
|
-
// and it's props, preventing either from being freed.
|
|
198
|
-
if (distringuish.isExternal(value.valueOf())) {
|
|
199
|
-
return distringuish.internalize(value.valueOf());
|
|
200
|
-
}
|
|
201
|
-
return value.valueOf();
|
|
192
|
+
return toUntrackedString(value);
|
|
202
193
|
}
|
|
203
194
|
|
|
204
195
|
if (_.isNumber(value)) {
|
|
@@ -12,277 +12,236 @@ Copyright: 2022 Contrast Security, Inc
|
|
|
12
12
|
engineered, modified, repackaged, sold, redistributed or otherwise used in a
|
|
13
13
|
way not consistent with the End User License Agreement.
|
|
14
14
|
*/
|
|
15
|
-
/**
|
|
16
|
-
* @module /lib/assess/sources
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
15
|
'use strict';
|
|
20
|
-
|
|
21
|
-
const
|
|
16
|
+
|
|
17
|
+
const parseurl = require('parseurl');
|
|
22
18
|
const agentEmitter = require('../../agent-emitter');
|
|
23
|
-
const SourceMembrane = require('../membrane/source-membrane');
|
|
24
19
|
const { AsyncStorage, KEYS } = require('../../core/async-storage');
|
|
25
|
-
const
|
|
26
|
-
const {
|
|
27
|
-
|
|
28
|
-
} = require('../../constants');
|
|
20
|
+
const logger = require('../../core/logger')('contrast:assess:sources');
|
|
21
|
+
const { EXCLUSION_INPUT_TYPES } = require('../../constants');
|
|
22
|
+
const SourceMembrane = require('../membrane/source-membrane');
|
|
29
23
|
const { Signature } = require('../models');
|
|
30
|
-
|
|
31
|
-
const sources = module.exports;
|
|
32
|
-
|
|
33
24
|
const { SourceEventHandler } = require('./event-handler');
|
|
25
|
+
const FormidableSource = require('./formidable');
|
|
34
26
|
|
|
35
27
|
/**
|
|
36
|
-
*
|
|
37
|
-
|
|
38
|
-
sources.init = function(agent) {
|
|
39
|
-
new FormidableSource(agent);
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* @param {string} type name of source type (headers, params, etc)
|
|
44
|
-
* @param {Object} parent object to watch property on, e.g., req
|
|
45
|
-
* @param {string} key name of property (for req.query, 'query')
|
|
46
|
-
* @param {Membrane} membrane used in testing to provide a custom membrane instance
|
|
28
|
+
* Shared logging for when URL/Input exclusions thwart handling of sources.
|
|
29
|
+
* @param {string} type the source type e.g. BODY, PARAMETER, HEADER
|
|
47
30
|
*/
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
parent[key] = membrane.wrap(object, metadata);
|
|
31
|
+
const logExclusionMessage = (type, exclusion) => {
|
|
32
|
+
const { name } = exclusion;
|
|
33
|
+
logger.debug(
|
|
34
|
+
'excluding %s inputs from all assess rules (%s)',
|
|
35
|
+
type.toLowerCase(),
|
|
36
|
+
name
|
|
37
|
+
);
|
|
56
38
|
};
|
|
57
39
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
40
|
+
module.exports = {
|
|
41
|
+
/**
|
|
42
|
+
* Registers sources for assess instrumentation
|
|
43
|
+
*/
|
|
44
|
+
init(agent) {
|
|
45
|
+
new FormidableSource(agent);
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {string} type name of source type (headers, params, etc)
|
|
50
|
+
* @param {Object} parent object to watch property on, e.g., req
|
|
51
|
+
* @param {string} key name of property (for req.query, 'query')
|
|
52
|
+
* @param {Membrane} membrane used in testing to provide a custom membrane instance
|
|
53
|
+
*/
|
|
54
|
+
track(type, parent, key, membrane) {
|
|
55
|
+
const object = parent[key];
|
|
56
|
+
if (!object) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const metadata = Object.create(null);
|
|
60
|
+
metadata.sourceType = type;
|
|
76
61
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
*/
|
|
83
|
-
sources.registerListeners = function({ config, exclusions }) {
|
|
84
|
-
const isLazyTrackingEnabled = sources.getLazyTrackingConfig(config, logger);
|
|
62
|
+
// We set this flag for string properties that are direct properties
|
|
63
|
+
// of the request object. Unlike parsed querystring values like
|
|
64
|
+
// { input: 'userInput' } these properties have an empty path, but still
|
|
65
|
+
// need to be tracked
|
|
66
|
+
metadata.rootLevelString = false;
|
|
85
67
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
return sources.handleSourceEvent(config, exclusions, BODY, obj, prop);
|
|
68
|
+
if (key === 'originalUrl') {
|
|
69
|
+
metadata.rootLevelString = true;
|
|
89
70
|
}
|
|
90
71
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (
|
|
102
|
-
return
|
|
72
|
+
parent[key] = membrane.wrap(object, metadata);
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Chooses a strategy for tracking the source events
|
|
77
|
+
* @param {any} config Current configuration for the agent
|
|
78
|
+
* @param {Logger} logger A logger instance
|
|
79
|
+
* @returns {Boolean} whether lazy tracking is enabled or not
|
|
80
|
+
*/
|
|
81
|
+
getLazyTrackingConfig(config, logger) {
|
|
82
|
+
if (config._default['agent.traverse_and_track']) {
|
|
83
|
+
return config.assess.enable_lazy_tracking;
|
|
103
84
|
}
|
|
104
85
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
data: obj[prop],
|
|
109
|
-
type: HEADER
|
|
110
|
-
});
|
|
111
|
-
});
|
|
112
|
-
agentEmitter.on('assess.params', (obj, prop) => {
|
|
113
|
-
if (isLazyTrackingEnabled) {
|
|
114
|
-
return sources.handleSourceEvent(
|
|
115
|
-
config,
|
|
116
|
-
exclusions,
|
|
117
|
-
PARAMETER,
|
|
118
|
-
obj,
|
|
119
|
-
prop
|
|
86
|
+
if (config._default['assess.enable_lazy_tracking']) {
|
|
87
|
+
logger.error(
|
|
88
|
+
"agent.traverse_and_track option is deprecated. Please use assess.enable_lazy_tracking from now on. It's value should be the opposite of this one"
|
|
120
89
|
);
|
|
90
|
+
return !config.agent.traverse_and_track;
|
|
121
91
|
}
|
|
122
92
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
93
|
+
logger.error(
|
|
94
|
+
'Conflicting options set: `agent.traverse_and_track` and `assess.enable_lazy_tracking`. `agent.traverse_and_track` is deprecated, so `assess.enable_lazy_tracking` takes precedence'
|
|
95
|
+
);
|
|
96
|
+
return config.assess.enable_lazy_tracking;
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Registers an event to add URL and input exclusions to async storage if they
|
|
101
|
+
* pertain to the current request path. Also registers all the source events
|
|
102
|
+
* that are emitted in the framework instrumentation sources. This wraps an
|
|
103
|
+
* object in a membrane
|
|
104
|
+
*/
|
|
105
|
+
registerListeners({ config, exclusions }) {
|
|
106
|
+
const isLazyTrackingEnabled = this.getLazyTrackingConfig(config, logger);
|
|
107
|
+
|
|
108
|
+
const handleAssessEvent = (event, type) => {
|
|
109
|
+
agentEmitter.on(event, (obj, prop) => {
|
|
110
|
+
if (isLazyTrackingEnabled) {
|
|
111
|
+
return this.handleSourceEvent(config, exclusions, type, obj, prop);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const signature = new Signature({
|
|
115
|
+
moduleName: 'Object',
|
|
116
|
+
methodName: 'getter',
|
|
117
|
+
args: [prop],
|
|
118
|
+
return: 'String',
|
|
119
|
+
isModule: false,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
new SourceEventHandler({ config, exclusions, signature, type }).handle({
|
|
123
|
+
obj,
|
|
124
|
+
prop,
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
handleAssessEvent('assess.url', EXCLUSION_INPUT_TYPES.PARAMETER);
|
|
130
|
+
handleAssessEvent('assess.body', EXCLUSION_INPUT_TYPES.BODY);
|
|
131
|
+
handleAssessEvent('assess.headers', EXCLUSION_INPUT_TYPES.HEADER);
|
|
132
|
+
handleAssessEvent('assess.params', EXCLUSION_INPUT_TYPES.PARAMETER);
|
|
133
|
+
handleAssessEvent('assess.query', EXCLUSION_INPUT_TYPES.QUERYSTRING);
|
|
134
|
+
handleAssessEvent('assess.cookies', EXCLUSION_INPUT_TYPES.COOKIE);
|
|
135
|
+
|
|
136
|
+
// might be helpful for clients to send add'l values in event arg
|
|
137
|
+
// - stackOpts: elide frames from function ref in client instrumentation
|
|
138
|
+
// - signature: rather than create shared one in the handler
|
|
139
|
+
// - or stack snapshot function - could share among SourceEvents
|
|
140
|
+
// - call context to share among SourceEvents
|
|
141
|
+
agentEmitter.on('assess.source', ({ obj, prop, type, signature }) => {
|
|
142
|
+
if (!signature) {
|
|
143
|
+
signature = new Signature({
|
|
144
|
+
moduleName: 'Object',
|
|
145
|
+
methodName: 'getter',
|
|
146
|
+
args: [prop],
|
|
147
|
+
return: 'String',
|
|
148
|
+
isModule: false,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
new SourceEventHandler({
|
|
133
153
|
config,
|
|
134
154
|
exclusions,
|
|
135
|
-
|
|
136
|
-
|
|
155
|
+
signature,
|
|
156
|
+
type,
|
|
157
|
+
stackOpts: undefined,
|
|
158
|
+
snapshot: undefined,
|
|
159
|
+
}).handle({ obj, prop });
|
|
160
|
+
});
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Checks whether exclusions settings should prevent wrapping of the untrusted
|
|
165
|
+
* data in membrane. If we should wrap, we create a membrane and provide it with
|
|
166
|
+
* active input exclusions to modify onString behaviors.
|
|
167
|
+
* @param {object} config agent config used to create membrane
|
|
168
|
+
* @param {ExclusionFactory} exclusions exclusions applicable to current req url
|
|
169
|
+
* @param {string} type source type
|
|
170
|
+
* @param {*} obj proxy target
|
|
171
|
+
* @param {string} prop property name
|
|
172
|
+
*/
|
|
173
|
+
handleSourceEvent(config, exclusions, type, obj, prop) {
|
|
174
|
+
const req = AsyncStorage.get(KEYS.REQ);
|
|
175
|
+
// NODE-1431: prevents crashing when req is not present in AsyncStorage.
|
|
176
|
+
if (!req) {
|
|
177
|
+
logger.error(
|
|
178
|
+
'failed to handle source event for type: %s, property: %s; req not present in async storage',
|
|
179
|
+
type,
|
|
137
180
|
prop
|
|
138
181
|
);
|
|
182
|
+
return;
|
|
139
183
|
}
|
|
140
184
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
type: COOKIE
|
|
158
|
-
});
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
// might be helpful for clients to send add'l values in event arg
|
|
162
|
-
// - stackOpts: elide frames from function ref in client instrumentation
|
|
163
|
-
// - signature: rather than create shared one in the handler
|
|
164
|
-
// - or stack snapshot function - could share among SourceEvents
|
|
165
|
-
// - call context to share among SourceEvents
|
|
166
|
-
agentEmitter.on('assess.source', ({ obj, prop, type, signature }) => {
|
|
167
|
-
if (!signature) {
|
|
168
|
-
signature = new Signature({
|
|
169
|
-
moduleName: 'Object',
|
|
170
|
-
methodName: 'getter',
|
|
171
|
-
args: [prop],
|
|
172
|
-
return: 'String',
|
|
173
|
-
isModule: false
|
|
174
|
-
});
|
|
185
|
+
const { pathname } = parseurl(req);
|
|
186
|
+
const urlExclusions = exclusions
|
|
187
|
+
.getUrlExclusions('assess')
|
|
188
|
+
.filter((e) => e.matchesUrl(pathname));
|
|
189
|
+
const inputExclusions = exclusions
|
|
190
|
+
.getInputExclusions('assess')
|
|
191
|
+
.filter((e) => e.matchesUrl(pathname) && e.appliesToInputType(type));
|
|
192
|
+
|
|
193
|
+
if (
|
|
194
|
+
this.exclusionSourceEventFilter({
|
|
195
|
+
type,
|
|
196
|
+
urlExclusions,
|
|
197
|
+
inputExclusions,
|
|
198
|
+
})
|
|
199
|
+
) {
|
|
200
|
+
return;
|
|
175
201
|
}
|
|
176
202
|
|
|
177
|
-
|
|
178
|
-
config,
|
|
179
|
-
exclusions,
|
|
180
|
-
signature,
|
|
181
|
-
type,
|
|
182
|
-
stackOpts: undefined,
|
|
183
|
-
snapshot: undefined
|
|
184
|
-
}).handle({ obj, prop });
|
|
185
|
-
});
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Checks whether exclusions settings should prevent wrapping of the untrusted
|
|
190
|
-
* data in membrane. If we should wrap, we create a membrane and provide it with
|
|
191
|
-
* active input exclusions to modify onString behaviors.
|
|
192
|
-
* @param {object} config agent config used to create membrane
|
|
193
|
-
* @param {ExclusionFactory} exclusions exclusions applicable to current req url
|
|
194
|
-
* @param {string} type source type
|
|
195
|
-
* @param {*} obj proxy target
|
|
196
|
-
* @param {string} prop property name
|
|
197
|
-
*/
|
|
198
|
-
sources.handleSourceEvent = function(config, exclusions, type, obj, prop) {
|
|
199
|
-
const req = AsyncStorage.get(KEYS.REQ);
|
|
200
|
-
// NODE-1431: prevents crashing when req is not present in AsyncStorage.
|
|
201
|
-
if (!req) {
|
|
202
|
-
logger.error(
|
|
203
|
-
'failed to handle source event for type: %s, property: %s; req not present in async storage',
|
|
203
|
+
const sourceParams = {
|
|
204
204
|
type,
|
|
205
|
-
prop
|
|
205
|
+
object: obj[prop],
|
|
206
|
+
constructorOpt: agentEmitter.emit,
|
|
207
|
+
inputExclusions,
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const membrane = new SourceMembrane(config, sourceParams);
|
|
211
|
+
this.track(type.toLowerCase(), obj, prop, membrane);
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Event filter function returns `false` if source event shouldn't be published.
|
|
216
|
+
* If there is a URL Exclusion for all rules, then don't bother tracking inputs.
|
|
217
|
+
* Otherwise, we have logic in TSReporter to skip reporting for URL Exclusions
|
|
218
|
+
* for specific rules.
|
|
219
|
+
* And some Input Exclusions apply broadly to the type. If they also apply to
|
|
220
|
+
* all rules we can skip the source event outright.
|
|
221
|
+
* @param {object} params
|
|
222
|
+
* @param {string} params.type
|
|
223
|
+
* @param {object[]} params.urlExclusions
|
|
224
|
+
* @param {object[]} params.inputExclusions
|
|
225
|
+
* @returns {boolean}
|
|
226
|
+
*/
|
|
227
|
+
exclusionSourceEventFilter({ type, urlExclusions, inputExclusions }) {
|
|
228
|
+
const urlMatch = urlExclusions.find((e) => e.appliesToAllAssessRules());
|
|
229
|
+
if (urlMatch) {
|
|
230
|
+
logExclusionMessage(type, urlMatch);
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
// `isNamed` is false for exclusion types such as BODY and QUERYSTRING which
|
|
234
|
+
// apply to the entire set of params not only those by a specified name. Also
|
|
235
|
+
// is true if the input name is set to wildcard "*" meaning it should apply to
|
|
236
|
+
// all values. In each case there's no need to wrap if all rules are excluded.
|
|
237
|
+
const inputMatch = inputExclusions.find(
|
|
238
|
+
(e) => e.appliesToAllAssessRules() && !e.isNamed
|
|
206
239
|
);
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const urlExclusions = exclusions
|
|
212
|
-
.getUrlExclusions('assess')
|
|
213
|
-
.filter((e) => e.matchesUrl(pathname));
|
|
214
|
-
const inputExclusions = exclusions
|
|
215
|
-
.getInputExclusions('assess')
|
|
216
|
-
.filter((e) => e.matchesUrl(pathname) && e.appliesToInputType(type));
|
|
217
|
-
|
|
218
|
-
if (
|
|
219
|
-
sources.exclusionSourceEventFilter({
|
|
220
|
-
type,
|
|
221
|
-
urlExclusions,
|
|
222
|
-
inputExclusions
|
|
223
|
-
})
|
|
224
|
-
) {
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const sourceParams = {
|
|
229
|
-
type,
|
|
230
|
-
object: obj[prop],
|
|
231
|
-
constructorOpt: agentEmitter.emit,
|
|
232
|
-
inputExclusions
|
|
233
|
-
};
|
|
234
|
-
|
|
235
|
-
const membrane = new SourceMembrane(config, sourceParams);
|
|
236
|
-
sources.track(type.toLowerCase(), obj, prop, membrane);
|
|
237
|
-
};
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Event filter function returns `false` if source event shouldn't be published.
|
|
241
|
-
* If there is a URL Exclusion for all rules, then don't bother tracking inputs.
|
|
242
|
-
* Otherwise, we have logic in TSReporter to skip reporting for URL Exclusions
|
|
243
|
-
* for specific rules.
|
|
244
|
-
* And some Input Exclusions apply broadly to the type. If they also apply to
|
|
245
|
-
* all rules we can skip the source event outright.
|
|
246
|
-
* @param {object} params
|
|
247
|
-
* @param {string} params.type
|
|
248
|
-
* @param {object[]} params.urlExclusions
|
|
249
|
-
* @param {object[]} params.inputExclusions
|
|
250
|
-
* @returns {boolean}
|
|
251
|
-
*/
|
|
252
|
-
sources.exclusionSourceEventFilter = function({
|
|
253
|
-
type,
|
|
254
|
-
urlExclusions,
|
|
255
|
-
inputExclusions
|
|
256
|
-
}) {
|
|
257
|
-
const urlMatch = urlExclusions.find((e) => e.appliesToAllAssessRules());
|
|
258
|
-
if (urlMatch) {
|
|
259
|
-
logExclusionMessage(type, urlMatch);
|
|
260
|
-
return true;
|
|
261
|
-
}
|
|
262
|
-
// `isNamed` is false for exclusion types such as BODY and QUERYSTRING which
|
|
263
|
-
// apply to the entire set of params not only those by a specified name. Also
|
|
264
|
-
// is true if the input name is set to wildcard "*" meaning it should apply to
|
|
265
|
-
// all values. In each case there's no need to wrap if all rules are excluded.
|
|
266
|
-
const inputMatch = inputExclusions.find(
|
|
267
|
-
(e) => e.appliesToAllAssessRules() && !e.isNamed
|
|
268
|
-
);
|
|
269
|
-
if (inputMatch) {
|
|
270
|
-
logExclusionMessage(type, inputMatch);
|
|
271
|
-
return true;
|
|
272
|
-
}
|
|
240
|
+
if (inputMatch) {
|
|
241
|
+
logExclusionMessage(type, inputMatch);
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
273
244
|
|
|
274
|
-
|
|
245
|
+
return false;
|
|
246
|
+
},
|
|
275
247
|
};
|
|
276
|
-
|
|
277
|
-
/**
|
|
278
|
-
* Shared logging for when URL/Input exclusions thwart handling of sources.
|
|
279
|
-
* @param {string} type the source type e.g. BODY, PARAMETER, HEADER
|
|
280
|
-
*/
|
|
281
|
-
function logExclusionMessage(type, exclusion) {
|
|
282
|
-
const { name } = exclusion;
|
|
283
|
-
logger.debug(
|
|
284
|
-
'excluding %s inputs from all assess rules (%s)',
|
|
285
|
-
type.toLowerCase(),
|
|
286
|
-
name
|
|
287
|
-
);
|
|
288
|
-
}
|
|
@@ -376,14 +376,16 @@ class ExpressFramework {
|
|
|
376
376
|
createMiddlewareWatchers() {
|
|
377
377
|
this.useAfter(function ContrastRequestReady(req, res, next) {
|
|
378
378
|
setFrameworkRequest(req, ExpressRequest);
|
|
379
|
+
|
|
379
380
|
agentEmitter.emit(
|
|
380
381
|
EVENTS.REQUEST_READY,
|
|
381
382
|
req,
|
|
382
383
|
res,
|
|
383
384
|
INPUT_TYPES.QUERYSTRING,
|
|
384
385
|
);
|
|
386
|
+
|
|
385
387
|
next();
|
|
386
|
-
}, '
|
|
388
|
+
}, 'expressInit');
|
|
387
389
|
|
|
388
390
|
// ... multer(multi-part form uploads) ...........................
|
|
389
391
|
this.useAfter(function ContrastMultiPartParsed(req, res, next) {
|
|
@@ -455,7 +457,7 @@ class ExpressFramework {
|
|
|
455
457
|
const params = new Object(this.params);
|
|
456
458
|
if (Object.keys(params).length) {
|
|
457
459
|
agentEmitter.emit(
|
|
458
|
-
EVENTS.
|
|
460
|
+
EVENTS.PARAMS_PARSED,
|
|
459
461
|
req,
|
|
460
462
|
res,
|
|
461
463
|
INPUT_TYPES.URL_PARAMETER,
|
package/lib/hooks/encoding.js
CHANGED
|
@@ -14,7 +14,7 @@ Copyright: 2022 Contrast Security, Inc
|
|
|
14
14
|
*/
|
|
15
15
|
'use strict';
|
|
16
16
|
|
|
17
|
-
const distringuish = require('@contrast/distringuish
|
|
17
|
+
const distringuish = require('@contrast/distringuish');
|
|
18
18
|
const logger = require('../core/logger')('contrast:hooks');
|
|
19
19
|
|
|
20
20
|
// some functions in the standard library allow users to specify a string
|
|
@@ -74,7 +74,7 @@ function hardPatchEncoding(obj, objName, methodName, strIndex) {
|
|
|
74
74
|
// check each possible index (encoding can be a number of arguments and)
|
|
75
75
|
// it depends on the function.
|
|
76
76
|
// force to desired encoding by decoding to a buffer and then re-encoding.
|
|
77
|
-
if (distringuish.isExternal(args[strIndex])) {
|
|
77
|
+
if (typeof args[strIndex] === 'string' && distringuish.isExternal(args[strIndex])) {
|
|
78
78
|
args[strIndex] = copy(args[strIndex]);
|
|
79
79
|
}
|
|
80
80
|
} catch (err) {
|
package/lib/list-installed.js
CHANGED
|
@@ -38,6 +38,7 @@ const execFile = util.promisify(require('child_process').execFile);
|
|
|
38
38
|
* @param {*} logger
|
|
39
39
|
* @returns {Promise<Result>}
|
|
40
40
|
*/
|
|
41
|
+
// eslint-disable-next-line complexity
|
|
41
42
|
module.exports = async function listInstalled(cwd, logger) {
|
|
42
43
|
const execFileOpts = {
|
|
43
44
|
cwd,
|
|
@@ -28,7 +28,7 @@ class ExpressSources {
|
|
|
28
28
|
agentEmitter.on(EVENTS.REQUEST_READY, ExpressSources.handleReady);
|
|
29
29
|
agentEmitter.on(EVENTS.BODY_PARSED, ExpressSources.handleBody);
|
|
30
30
|
agentEmitter.on(EVENTS.COOKIES_PARSED, ExpressSources.handleCookies);
|
|
31
|
-
agentEmitter.on(EVENTS.
|
|
31
|
+
agentEmitter.on(EVENTS.PARAMS_PARSED, ExpressSources.handleParams);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
static handleReady(req, res, inputType) {
|
|
@@ -169,7 +169,7 @@ class Speedracer {
|
|
|
169
169
|
this.logger.info('starting contrast-service');
|
|
170
170
|
this.startTime = Date.now();
|
|
171
171
|
|
|
172
|
-
|
|
172
|
+
let speedracerPath = path.resolve(
|
|
173
173
|
__dirname,
|
|
174
174
|
'..',
|
|
175
175
|
'..',
|
|
@@ -179,6 +179,10 @@ class Speedracer {
|
|
|
179
179
|
`contrast-service-${process.platform}-${process.arch}`
|
|
180
180
|
);
|
|
181
181
|
|
|
182
|
+
if (process.platform === 'win32') {
|
|
183
|
+
speedracerPath = `${speedracerPath}.exe`;
|
|
184
|
+
}
|
|
185
|
+
|
|
182
186
|
try {
|
|
183
187
|
fs.statSync(speedracerPath);
|
|
184
188
|
} catch (error) {
|
package/lib/tracker.js
CHANGED
|
@@ -12,188 +12,137 @@ Copyright: 2022 Contrast Security, Inc
|
|
|
12
12
|
engineered, modified, repackaged, sold, redistributed or otherwise used in a
|
|
13
13
|
way not consistent with the End User License Agreement.
|
|
14
14
|
*/
|
|
15
|
-
/**
|
|
16
|
-
* Tracker for objects we want to persist contrastProperties for.
|
|
17
|
-
*
|
|
18
|
-
* @module lib/tracker
|
|
19
|
-
*/
|
|
20
15
|
'use strict';
|
|
21
16
|
|
|
22
17
|
const logger = require('./core/logger')('contrast:tracker');
|
|
23
|
-
const distringuish = require('@contrast/distringuish
|
|
24
|
-
|
|
25
|
-
const defaultContrastProperties = {
|
|
26
|
-
get tracked() {
|
|
27
|
-
return false;
|
|
28
|
-
},
|
|
29
|
-
set tracked(arg) {
|
|
30
|
-
logger.warn('tracked assigned to default contrastProperties');
|
|
31
|
-
},
|
|
32
|
-
get tagRanges() {
|
|
33
|
-
logger.warn('tagRanges referenced from default contrastProperties');
|
|
34
|
-
return [];
|
|
35
|
-
},
|
|
36
|
-
set tagRanges(arg) {
|
|
37
|
-
logger.warn('tagRanges assigned to default contrastProperties');
|
|
38
|
-
},
|
|
39
|
-
get event() {
|
|
40
|
-
logger.warn('event referenced from default contrastProperties');
|
|
41
|
-
return null;
|
|
42
|
-
},
|
|
43
|
-
set event(arg) {
|
|
44
|
-
logger.warn('event assigned to default contrastProperties');
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
// i'm not sure why this is a class. there are no methods, and externalized
|
|
49
|
-
// strings don't have an instance of the class; they have an object with the
|
|
50
|
-
// same property names.
|
|
51
|
-
class ContrastProperties {
|
|
52
|
-
constructor() {
|
|
53
|
-
this.event = null;
|
|
54
|
-
this.tagRanges = [];
|
|
55
|
-
this.tracked = true;
|
|
56
|
-
}
|
|
57
|
-
// this is used to populate the object created by externalize.
|
|
58
|
-
static populate(obj) {
|
|
59
|
-
obj.event = null;
|
|
60
|
-
obj.tagRanges = [];
|
|
61
|
-
obj.tracked = true;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
18
|
+
const distringuish = require('@contrast/distringuish');
|
|
64
19
|
|
|
65
20
|
class Tracker {
|
|
66
21
|
constructor() {
|
|
67
|
-
|
|
22
|
+
/** @type {WeakMap<String, ContrastProperties>} */
|
|
68
23
|
this.metadata = new WeakMap();
|
|
69
24
|
}
|
|
70
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Externalizes a given primitive string, attaching a ContrastProperties
|
|
28
|
+
* object. Returns null if the string is unable to be externalized.
|
|
29
|
+
* @param {string} str
|
|
30
|
+
* @returns {{ str: str | string, props: ContrastProperties} | null}
|
|
31
|
+
*/
|
|
71
32
|
trackString(str) {
|
|
72
|
-
|
|
73
|
-
|
|
33
|
+
let props = distringuish.getProperties(str);
|
|
34
|
+
if (props) {
|
|
35
|
+
return { str, props };
|
|
74
36
|
}
|
|
75
37
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
38
|
+
str = distringuish.externalize(str);
|
|
39
|
+
|
|
40
|
+
if (typeof str === 'number') {
|
|
41
|
+
switch (str) {
|
|
42
|
+
case distringuish.ExternalizeErrorCode.ZERO_LENGTH_STRING:
|
|
43
|
+
logger.warn('Cannot externalize and track empty strings.'); // we should never see this error
|
|
44
|
+
return null;
|
|
45
|
+
case distringuish.ExternalizeErrorCode.INVALID_STRING_ENCODING:
|
|
46
|
+
logger.error('Unable to externalize non-two-byte strings.');
|
|
47
|
+
return null;
|
|
48
|
+
case distringuish.ExternalizeErrorCode.TO_LOCAL_FAILURE:
|
|
49
|
+
logger.error(
|
|
50
|
+
'An error occurred when creating a new external string.'
|
|
51
|
+
);
|
|
52
|
+
return null;
|
|
53
|
+
case distringuish.ExternalizeErrorCode.EXTERNALIZATION_FAILURE:
|
|
54
|
+
logger.error('The provided string was unable to be externalized.');
|
|
55
|
+
return null;
|
|
56
|
+
default:
|
|
57
|
+
logger.error('An unknown error occurred');
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
81
60
|
}
|
|
82
61
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
Object.assign(props, data);
|
|
62
|
+
props = distringuish.getProperties(str);
|
|
63
|
+
Object.assign(props, {
|
|
64
|
+
event: null,
|
|
65
|
+
tagRanges: [],
|
|
66
|
+
tracked: true,
|
|
67
|
+
});
|
|
90
68
|
|
|
91
|
-
return
|
|
69
|
+
return { str, props };
|
|
92
70
|
}
|
|
93
71
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (this.metadata.has(
|
|
101
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Tracks a given String object using the `metadata` WeakMap.
|
|
74
|
+
* @param {String} str
|
|
75
|
+
* @returns {str}
|
|
76
|
+
*/
|
|
77
|
+
trackStringObject(str) {
|
|
78
|
+
if (!this.metadata.has(str)) {
|
|
79
|
+
this.metadata.set(str, {
|
|
80
|
+
event: null,
|
|
81
|
+
tagRanges: [],
|
|
82
|
+
tracked: true,
|
|
83
|
+
});
|
|
102
84
|
}
|
|
103
85
|
|
|
104
|
-
this.metadata.
|
|
105
|
-
|
|
106
|
-
return value;
|
|
86
|
+
return { str, props: this.metadata.get(str) };
|
|
107
87
|
}
|
|
108
88
|
|
|
109
|
-
|
|
110
89
|
/**
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* @
|
|
115
|
-
* @returns {Object|null} {str, props} or null on error.
|
|
90
|
+
* Return the properties associated with a given string. Returns null if the
|
|
91
|
+
* string has not previously been tracked.
|
|
92
|
+
* @param {string | String} str
|
|
93
|
+
* @return {ContrastProperties | null}
|
|
116
94
|
*/
|
|
117
|
-
|
|
95
|
+
getData(str) {
|
|
96
|
+
let prop;
|
|
118
97
|
if (typeof str === 'string') {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
return { str, props };
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
str = distringuish.externalize(str);
|
|
126
|
-
if (!str) {
|
|
127
|
-
return null;
|
|
128
|
-
}
|
|
129
|
-
props = distringuish.getProperties(str);
|
|
130
|
-
if (!props) {
|
|
131
|
-
return null;
|
|
132
|
-
}
|
|
133
|
-
ContrastProperties.populate(props);
|
|
134
|
-
|
|
135
|
-
return { str, props };
|
|
98
|
+
prop = distringuish.getProperties(str);
|
|
99
|
+
} else if (str instanceof String) {
|
|
100
|
+
prop = this.metadata.get(str);
|
|
136
101
|
}
|
|
137
102
|
|
|
138
|
-
|
|
139
|
-
// no duplicates
|
|
140
|
-
let props = this.metadata.get(str);
|
|
141
|
-
if (props) {
|
|
142
|
-
return { str, props };
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
props = new ContrastProperties();
|
|
146
|
-
this.metadata.set(str, props);
|
|
147
|
-
|
|
148
|
-
return { str, props };
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return null;
|
|
103
|
+
return prop || null;
|
|
152
104
|
}
|
|
153
105
|
|
|
154
106
|
/**
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
* @param {
|
|
158
|
-
* @
|
|
107
|
+
* Associate properties with a string. Returns null if str is not a string,
|
|
108
|
+
* is a zero-length string, or any internal error takes place.
|
|
109
|
+
* @param {string | String} str
|
|
110
|
+
* @returns {{ str: str | string, props: ContrastProperties} | null}
|
|
159
111
|
*/
|
|
160
|
-
|
|
161
|
-
if (typeof str === 'string') {
|
|
162
|
-
return
|
|
112
|
+
track(str) {
|
|
113
|
+
if (typeof str === 'string' && str.length > 0) {
|
|
114
|
+
return this.trackString(str);
|
|
163
115
|
}
|
|
164
|
-
|
|
165
|
-
|
|
116
|
+
|
|
117
|
+
if (str instanceof String && str.length > 0) {
|
|
118
|
+
return this.trackStringObject(str);
|
|
166
119
|
}
|
|
120
|
+
|
|
167
121
|
return null;
|
|
168
122
|
}
|
|
169
123
|
|
|
170
124
|
/**
|
|
171
|
-
* Resets a string's
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
125
|
+
* Resets a string's tracked data metadata to the default contrast properties.
|
|
126
|
+
* If the string is a primitive, we return a new, non-externalized copy of the
|
|
127
|
+
* string.
|
|
128
|
+
* If the string was an Object, we remove the properties from the `metadata`
|
|
129
|
+
* WeakMap.
|
|
130
|
+
* @param {string | String} str
|
|
131
|
+
* @returns {str | null}
|
|
175
132
|
*/
|
|
176
133
|
untrack(str) {
|
|
177
134
|
if (typeof str === 'string') {
|
|
178
|
-
let props = distringuish.getProperties(str);
|
|
179
|
-
if (!props) {
|
|
180
|
-
return null;
|
|
181
|
-
}
|
|
182
|
-
// return an untracked version of the string
|
|
183
|
-
Object.assign(props, {event: null, tagRanges: [], tracked: false})
|
|
184
135
|
return distringuish.internalize(str);
|
|
185
136
|
}
|
|
137
|
+
|
|
186
138
|
if (str instanceof String) {
|
|
187
|
-
|
|
188
|
-
return null;
|
|
189
|
-
}
|
|
139
|
+
this.metadata.delete(str);
|
|
190
140
|
return str;
|
|
191
141
|
}
|
|
142
|
+
|
|
192
143
|
return null;
|
|
193
144
|
}
|
|
194
145
|
}
|
|
195
146
|
|
|
196
|
-
|
|
197
|
-
module.exports = tracker;
|
|
147
|
+
module.exports = new Tracker();
|
|
198
148
|
module.exports.Tracker = Tracker;
|
|
199
|
-
module.exports.defaultContrastProperties = defaultContrastProperties;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contrast/agent",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.25.1",
|
|
4
4
|
"description": "Node.js security instrumentation by Contrast Security",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"security",
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
"@babel/traverse": "^7.12.1",
|
|
78
78
|
"@babel/types": "^7.12.1",
|
|
79
79
|
"@contrast/agent-lib": "^4.3.0",
|
|
80
|
-
"@contrast/distringuish
|
|
80
|
+
"@contrast/distringuish": "^4.0.0",
|
|
81
81
|
"@contrast/flat": "^4.1.1",
|
|
82
82
|
"@contrast/fn-inspect": "^3.1.0",
|
|
83
83
|
"@contrast/protobuf-api": "^3.2.5",
|
|
@@ -193,7 +193,7 @@
|
|
|
193
193
|
"test": "test"
|
|
194
194
|
},
|
|
195
195
|
"engines": {
|
|
196
|
-
"node": ">=12.13.0 <13 || >=14.15.0 <15 || >=16.9.1 <17",
|
|
196
|
+
"node": ">=12.13.0 <13 || >=14.15.0 <15 || >=16.9.1 <17 || >=18.7.0 <19",
|
|
197
197
|
"npm": ">=6.13.7 <7 || >=7.11.0"
|
|
198
198
|
},
|
|
199
199
|
"bundleDependencies": [
|