@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.
- package/bootstrap.js +12 -2
- package/esm.mjs +33 -0
- package/lib/assess/index.js +2 -0
- package/lib/assess/models/source-event.js +6 -0
- package/lib/assess/policy/rules.json +29 -0
- package/lib/assess/policy/signatures.json +6 -0
- package/lib/assess/propagators/JSON/stringify.js +77 -7
- package/lib/assess/sinks/rethinkdb-nosql-injection.js +142 -0
- package/lib/assess/sources/event-handler.js +307 -0
- package/lib/assess/sources/index.js +93 -5
- package/lib/assess/spdy/index.js +23 -0
- package/lib/assess/spdy/sinks/index.js +23 -0
- package/lib/assess/spdy/sinks/xss.js +84 -0
- package/lib/assess/technologies/index.js +2 -1
- package/lib/constants.js +2 -1
- package/lib/contrast.js +6 -6
- package/lib/core/arch-components/index.js +1 -0
- package/lib/core/arch-components/mongodb.js +22 -18
- package/lib/core/arch-components/postgres.js +21 -3
- package/lib/core/arch-components/sqlite3.js +3 -5
- package/lib/core/config/options.js +35 -1
- package/lib/core/exclusions/exclusion.js +2 -5
- package/lib/core/express/index.js +25 -2
- package/lib/core/express/utils.js +8 -3
- package/lib/hooks/frameworks/index.js +2 -0
- package/lib/hooks/frameworks/spdy.js +87 -0
- package/lib/hooks/http.js +11 -0
- package/lib/reporter/translations/to-protobuf/dtm/trace-event/index.js +4 -4
- package/package.json +11 -6
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/lib/constants.js
CHANGED
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
|
-
|
|
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: ${
|
|
247
|
-
logger.info(`Modified argv: ${
|
|
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 ?
|
|
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
|
-
?
|
|
343
|
-
:
|
|
342
|
+
? program.args.slice(1)
|
|
343
|
+
: program.args;
|
|
344
344
|
|
|
345
345
|
try {
|
|
346
346
|
enabled = await contrastAgent.prepare(options, commanderArgs, isCli);
|
|
@@ -28,25 +28,29 @@ ModuleHook.resolve(
|
|
|
28
28
|
patchType: PATCH_TYPES.ARCH_COMPONENT,
|
|
29
29
|
alwaysRun: true,
|
|
30
30
|
post(ctx) {
|
|
31
|
-
|
|
32
|
-
|
|
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
|
}
|