@contrast/assess 1.7.0 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/dataflow/event-factory.js +17 -13
- package/lib/dataflow/propagation/index.js +5 -0
- package/lib/dataflow/propagation/install/JSON/index.js +34 -0
- package/lib/dataflow/propagation/install/JSON/parse-fn.js +248 -0
- package/lib/dataflow/propagation/install/JSON/parse.js +196 -0
- package/lib/dataflow/propagation/install/JSON/stringify.js +292 -0
- package/lib/dataflow/propagation/install/array-prototype-join.js +21 -14
- package/lib/dataflow/propagation/install/buffer.js +81 -0
- package/lib/dataflow/propagation/install/contrast-methods/add.js +2 -0
- package/lib/dataflow/propagation/install/contrast-methods/index.js +1 -0
- package/lib/dataflow/propagation/install/contrast-methods/number.js +58 -0
- package/lib/dataflow/propagation/install/contrast-methods/string.js +53 -6
- package/lib/dataflow/propagation/install/contrast-methods/tag.js +3 -1
- package/lib/dataflow/propagation/install/decode-uri-component.js +9 -2
- package/lib/dataflow/propagation/install/ejs/escape-xml.js +8 -2
- package/lib/dataflow/propagation/install/encode-uri-component.js +9 -2
- package/lib/dataflow/propagation/install/escape-html.js +13 -5
- package/lib/dataflow/propagation/install/escape.js +9 -2
- package/lib/dataflow/propagation/install/handlebars-utils-escape-expression.js +11 -4
- package/lib/dataflow/propagation/install/isnumeric-0.js +59 -0
- package/lib/dataflow/propagation/install/mongoose/index.js +36 -0
- package/lib/dataflow/propagation/install/mongoose/schema-string.js +156 -0
- package/lib/dataflow/propagation/install/mysql-connection-escape.js +5 -0
- package/lib/dataflow/propagation/install/parse-int.js +60 -0
- package/lib/dataflow/propagation/install/pug-runtime-escape.js +9 -2
- package/lib/dataflow/propagation/install/querystring/parse.js +11 -9
- package/lib/dataflow/propagation/install/sequelize.js +6 -3
- package/lib/dataflow/propagation/install/sql-template-strings.js +9 -2
- package/lib/dataflow/propagation/install/string/concat.js +8 -2
- package/lib/dataflow/propagation/install/string/format-methods.js +7 -2
- package/lib/dataflow/propagation/install/string/html-methods.js +15 -5
- package/lib/dataflow/propagation/install/string/match.js +14 -9
- package/lib/dataflow/propagation/install/string/replace.js +22 -14
- package/lib/dataflow/propagation/install/string/slice.js +13 -5
- package/lib/dataflow/propagation/install/string/split.js +15 -11
- package/lib/dataflow/propagation/install/string/substring.js +16 -6
- package/lib/dataflow/propagation/install/string/trim.js +3 -0
- package/lib/dataflow/propagation/install/unescape.js +9 -2
- package/lib/dataflow/propagation/install/url/domain-parsers.js +7 -2
- package/lib/dataflow/propagation/install/validator/hooks.js +6 -2
- package/lib/dataflow/sinks/install/child-process.js +116 -50
- package/lib/dataflow/sinks/install/express/unvalidated-redirect.js +6 -3
- package/lib/dataflow/sinks/install/fastify/unvalidated-redirect.js +7 -4
- package/lib/dataflow/sinks/install/fs.js +44 -12
- package/lib/dataflow/sinks/install/http.js +5 -2
- package/lib/dataflow/sinks/install/koa/unvalidated-redirect.js +7 -4
- package/lib/dataflow/sinks/install/marsdb.js +3 -0
- package/lib/dataflow/sinks/install/mongodb.js +249 -149
- package/lib/dataflow/sinks/install/mssql.js +9 -2
- package/lib/dataflow/sinks/install/mysql.js +9 -4
- package/lib/dataflow/sinks/install/postgres.js +6 -3
- package/lib/dataflow/sinks/install/sequelize.js +7 -5
- package/lib/dataflow/sinks/install/sqlite3.js +7 -3
- package/lib/dataflow/sources/handler.js +141 -26
- package/lib/dataflow/sources/index.js +2 -7
- package/lib/dataflow/sources/install/body-parser1.js +19 -6
- package/lib/dataflow/sources/install/express/index.js +4 -1
- package/lib/dataflow/sources/install/express/params.js +81 -0
- package/lib/dataflow/sources/install/express/parsedUrl.js +87 -0
- package/lib/dataflow/sources/install/http.js +33 -19
- package/lib/dataflow/sources/install/querystring.js +75 -0
- package/lib/dataflow/tag-utils.js +92 -1
- package/lib/dataflow/tracker.js +6 -6
- package/lib/index.js +2 -0
- package/lib/response-scanning/handlers/utils.js +2 -2
- package/lib/session-configuration/index.js +34 -0
- package/lib/session-configuration/install/http.js +79 -0
- package/package.json +2 -2
|
@@ -19,7 +19,7 @@ const {
|
|
|
19
19
|
InputType,
|
|
20
20
|
DataflowTag,
|
|
21
21
|
isString,
|
|
22
|
-
|
|
22
|
+
join,
|
|
23
23
|
} = require('@contrast/common');
|
|
24
24
|
|
|
25
25
|
module.exports = function(core) {
|
|
@@ -28,17 +28,17 @@ module.exports = function(core) {
|
|
|
28
28
|
dataflow: {
|
|
29
29
|
sources,
|
|
30
30
|
tracker,
|
|
31
|
-
eventFactory
|
|
31
|
+
eventFactory
|
|
32
32
|
}
|
|
33
33
|
},
|
|
34
|
-
createSnapshot,
|
|
35
34
|
config,
|
|
35
|
+
createSnapshot,
|
|
36
36
|
logger,
|
|
37
37
|
} = core;
|
|
38
38
|
|
|
39
39
|
const emptyStack = Object.freeze([]);
|
|
40
40
|
|
|
41
|
-
sources.createTags = function createTags({ inputType,
|
|
41
|
+
sources.createTags = function createTags({ inputType, fieldName = '', value }) {
|
|
42
42
|
if (!value?.length) {
|
|
43
43
|
return null;
|
|
44
44
|
}
|
|
@@ -48,25 +48,30 @@ module.exports = function(core) {
|
|
|
48
48
|
[DataflowTag.UNTRUSTED]: [0, stop]
|
|
49
49
|
};
|
|
50
50
|
|
|
51
|
-
if (inputType === InputType.HEADER &&
|
|
51
|
+
if (inputType === InputType.HEADER && fieldName.toLowerCase() === 'referer') {
|
|
52
52
|
tags[DataflowTag.HEADER] = [0, stop];
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
return tags;
|
|
56
56
|
};
|
|
57
57
|
|
|
58
|
+
sources.createStacktrace = function(stacktraceOpts) {
|
|
59
|
+
return config.assess.stacktraces === 'NONE'
|
|
60
|
+
? emptyStack
|
|
61
|
+
: createSnapshot(stacktraceOpts)();
|
|
62
|
+
};
|
|
63
|
+
|
|
58
64
|
sources.handle = function({
|
|
59
65
|
context,
|
|
66
|
+
keys,
|
|
60
67
|
name,
|
|
61
68
|
inputType = InputType.UNKNOWN,
|
|
62
69
|
stacktraceOpts,
|
|
63
70
|
data,
|
|
64
|
-
sourceContext
|
|
71
|
+
sourceContext,
|
|
65
72
|
}) {
|
|
66
73
|
if (!data) return;
|
|
67
74
|
|
|
68
|
-
const max = config.assess.max_context_source_events;
|
|
69
|
-
|
|
70
75
|
if (!sourceContext) {
|
|
71
76
|
core.logger.trace({ inputType, name }, 'skipping assess source handling - no request context');
|
|
72
77
|
return null;
|
|
@@ -76,31 +81,60 @@ module.exports = function(core) {
|
|
|
76
81
|
context = inputType;
|
|
77
82
|
}
|
|
78
83
|
|
|
84
|
+
const max = config.assess.max_context_source_events;
|
|
85
|
+
let _data = data;
|
|
79
86
|
let stack;
|
|
80
87
|
|
|
81
|
-
|
|
88
|
+
if (keys) {
|
|
89
|
+
_data = {};
|
|
90
|
+
for (const key of keys) {
|
|
91
|
+
_data[key] = data[key];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function createEvent({ fieldName, pathName, value }) {
|
|
96
|
+
// create the stacktrace once per call to .handle()
|
|
97
|
+
stack || (stack = sources.createStacktrace(stacktraceOpts));
|
|
98
|
+
return eventFactory.createSourceEvent({
|
|
99
|
+
context: `${context}.${pathName}`,
|
|
100
|
+
name,
|
|
101
|
+
fieldName,
|
|
102
|
+
pathName,
|
|
103
|
+
stack,
|
|
104
|
+
inputType,
|
|
105
|
+
tags: sources.createTags({ inputType, fieldName, value }),
|
|
106
|
+
result: { tracked: true, value },
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (Buffer.isBuffer(data) && !tracker.getData(data)) {
|
|
111
|
+
const event = createEvent({ pathName: 'body', value: data, fieldName: '' });
|
|
112
|
+
if (event) {
|
|
113
|
+
tracker.track(data, event);
|
|
114
|
+
}
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
traverse(_data, (path, fieldName, value, obj) => {
|
|
119
|
+
const pathName = join(path, '.');
|
|
120
|
+
|
|
82
121
|
if (sourceContext.sourceEventsCount >= max) {
|
|
83
122
|
core.logger.trace({ inputType, name }, 'exiting assess source handling - %s max events exceeded', max);
|
|
84
123
|
return true;
|
|
85
124
|
}
|
|
86
125
|
|
|
87
126
|
if (isString(value) && value.length) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
pathName,
|
|
98
|
-
stack,
|
|
99
|
-
inputType,
|
|
100
|
-
tags: sources.createTags({ inputType, key, value }),
|
|
101
|
-
result: { tracked: true, value },
|
|
102
|
-
});
|
|
127
|
+
const strInfo = tracker.getData(value);
|
|
128
|
+
|
|
129
|
+
if (strInfo) {
|
|
130
|
+
// TODO: confirm this "layering-on" approach is what we want
|
|
131
|
+
// when the value is tracked the handler wins out and we "re-tracks" the value with new source
|
|
132
|
+
// event metadata. without this step tracker would complain about value already being tracked.
|
|
133
|
+
// alternatively we could treat this more like a propagation event and update existing metadata.
|
|
134
|
+
value = strInfo.value;
|
|
135
|
+
}
|
|
103
136
|
|
|
137
|
+
const event = createEvent({ pathName, value, fieldName });
|
|
104
138
|
if (!event) {
|
|
105
139
|
core.logger.warn({ inputType, name, pathName, value }, 'unable to create source event');
|
|
106
140
|
return;
|
|
@@ -108,15 +142,96 @@ module.exports = function(core) {
|
|
|
108
142
|
|
|
109
143
|
const { extern } = tracker.track(value, event);
|
|
110
144
|
if (extern) {
|
|
111
|
-
logger.trace({ extern,
|
|
112
|
-
obj[
|
|
145
|
+
logger.trace({ extern, fieldName, name, inputType }, 'tracked');
|
|
146
|
+
obj[fieldName] = extern;
|
|
147
|
+
|
|
113
148
|
sourceContext.sourceEventsCount++;
|
|
114
149
|
}
|
|
150
|
+
} else if (Buffer.isBuffer(value) && !tracker.getData(value)) {
|
|
151
|
+
const event = createEvent({ pathName, value, fieldName });
|
|
152
|
+
if (event) {
|
|
153
|
+
tracker.track(value, event);
|
|
154
|
+
} else {
|
|
155
|
+
core.logger.warn({ inputType, name, pathName, value }, 'unable to create source event');
|
|
156
|
+
}
|
|
115
157
|
}
|
|
116
158
|
});
|
|
117
159
|
|
|
160
|
+
if (keys) {
|
|
161
|
+
for (const key of keys) {
|
|
162
|
+
data[key] = _data[key];
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
118
166
|
return data;
|
|
119
167
|
};
|
|
120
168
|
|
|
121
169
|
return sources;
|
|
122
170
|
};
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* A custom traversal function for handling source value tracking efficiently.
|
|
174
|
+
* Implementation was adapted from traversal methods in @contrast/common.
|
|
175
|
+
* @param {any} target object to traverse
|
|
176
|
+
* @param {function} cb function<path, key, value, obj>
|
|
177
|
+
* @param {string[]} path path of node being visted; constructed of nested keys
|
|
178
|
+
* @param {boolean} halt whether to halt traversal; determined by callback
|
|
179
|
+
* @param {Set} visited used to dedupe circular references
|
|
180
|
+
*/
|
|
181
|
+
function traverse(target, cb, path = [], visited = new Set()) {
|
|
182
|
+
if (isTraversable(target)) {
|
|
183
|
+
for (const key in target) {
|
|
184
|
+
path.push(key);
|
|
185
|
+
|
|
186
|
+
const value = target[key];
|
|
187
|
+
|
|
188
|
+
if (visited.has(value)) {
|
|
189
|
+
path.pop();
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (isVisitable(value)) {
|
|
194
|
+
const halt = cb(path, key, value, target) === false;
|
|
195
|
+
if (halt) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (isTraversable(value)) {
|
|
201
|
+
visited.add(value);
|
|
202
|
+
traverse(value, cb, path, visited);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
path.pop();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Visit strings, buffers, basic objects and arrays.
|
|
212
|
+
* @param {any} value the value to check
|
|
213
|
+
* @returns {boolean}
|
|
214
|
+
*/
|
|
215
|
+
function isVisitable(value) {
|
|
216
|
+
if (!value) return false;
|
|
217
|
+
|
|
218
|
+
return value.constructor?.name === 'String' ||
|
|
219
|
+
value.constructor?.name === 'Buffer' ||
|
|
220
|
+
(typeof value === 'object' && !value.constructor);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* The criteria for traversal is a strict as possible. We only traverse plain
|
|
225
|
+
* objects and arrays and objects created via Object.create(null).
|
|
226
|
+
* @param {any} value the value to check
|
|
227
|
+
* @returns {boolean}
|
|
228
|
+
*/
|
|
229
|
+
function isTraversable(value) {
|
|
230
|
+
if (!value || typeof value !== 'object') return false;
|
|
231
|
+
|
|
232
|
+
return value.constructor?.name === 'Object' ||
|
|
233
|
+
value.constructor?.name === 'Array' ||
|
|
234
|
+
!value.constructor;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
module.exports.traverse = traverse;
|
|
@@ -20,23 +20,18 @@ const { callChildComponentMethodsSync } = require('@contrast/common');
|
|
|
20
20
|
module.exports = function (core) {
|
|
21
21
|
const sources = core.assess.dataflow.sources = {};
|
|
22
22
|
|
|
23
|
-
// API
|
|
24
23
|
require('./handler')(core);
|
|
25
|
-
// installers
|
|
26
|
-
// general
|
|
27
|
-
require('./install/http')(core);
|
|
28
24
|
|
|
29
|
-
// frameworks and frameworks specific libraries
|
|
30
25
|
require('./install/express')(core);
|
|
31
26
|
require('./install/fastify')(core);
|
|
32
27
|
require('./install/koa')(core);
|
|
33
|
-
|
|
34
|
-
// libraries
|
|
35
28
|
require('./install/body-parser1')(core);
|
|
36
29
|
require('./install/busboy1')(core);
|
|
37
30
|
require('./install/cookie-parser1')(core);
|
|
38
31
|
require('./install/formidable1')(core);
|
|
32
|
+
require('./install/http')(core);
|
|
39
33
|
require('./install/qs6')(core);
|
|
34
|
+
require('./install/querystring')(core);
|
|
40
35
|
|
|
41
36
|
sources.install = function install() {
|
|
42
37
|
callChildComponentMethodsSync(sources, 'install');
|
|
@@ -36,12 +36,12 @@ module.exports = function init(core) {
|
|
|
36
36
|
|
|
37
37
|
if (!sourceContext) {
|
|
38
38
|
logger.error({ name }, 'unable to handle source. Missing `sourceContext`');
|
|
39
|
-
return;
|
|
39
|
+
return next(...args);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
if (sourceContext.parsedBody) {
|
|
43
43
|
logger.trace({ name }, 'values already tracked');
|
|
44
|
-
return;
|
|
44
|
+
return next(...args);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
// when using a specific parser, we know the input type.
|
|
@@ -55,19 +55,32 @@ module.exports = function init(core) {
|
|
|
55
55
|
: InputType.BODY;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
let keys;
|
|
59
|
+
let _data = req.body;
|
|
60
|
+
let context = 'req.body';
|
|
61
|
+
const isString = typeof _data === 'string';
|
|
62
|
+
const isBuffer = Buffer.isBuffer(_data);
|
|
63
|
+
|
|
64
|
+
if (isString || isBuffer) {
|
|
65
|
+
context = 'req';
|
|
66
|
+
keys = ['body'];
|
|
67
|
+
_data = req;
|
|
68
|
+
}
|
|
69
|
+
|
|
58
70
|
try {
|
|
59
71
|
assess.dataflow.sources.handle({
|
|
60
|
-
context
|
|
72
|
+
context,
|
|
73
|
+
data: _data,
|
|
61
74
|
name,
|
|
75
|
+
keys,
|
|
62
76
|
inputType,
|
|
77
|
+
sourceContext,
|
|
63
78
|
stacktraceOpts: {
|
|
64
79
|
constructorOpt: contrastNext
|
|
65
80
|
},
|
|
66
|
-
data: req.body,
|
|
67
|
-
sourceContext,
|
|
68
81
|
});
|
|
69
82
|
|
|
70
|
-
sourceContext.parsedBody = !!Object.keys(
|
|
83
|
+
sourceContext.parsedBody = !!Object.keys(_data).length;
|
|
71
84
|
} catch (err) {
|
|
72
85
|
logger.error({ name, err }, 'unable to handle source');
|
|
73
86
|
}
|
|
@@ -17,12 +17,15 @@
|
|
|
17
17
|
|
|
18
18
|
const { callChildComponentMethodsSync } = require('@contrast/common');
|
|
19
19
|
|
|
20
|
-
module.exports = function
|
|
20
|
+
module.exports = function (core) {
|
|
21
21
|
core.assess.dataflow.sources.expressInstrumentation = {
|
|
22
22
|
install() {
|
|
23
23
|
callChildComponentMethodsSync(this, 'install');
|
|
24
24
|
}
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
+
require('./params')(core);
|
|
28
|
+
require('./parsedUrl')(core);
|
|
29
|
+
|
|
27
30
|
return core.assess.dataflow.sources.expressInstrumentation;
|
|
28
31
|
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright: 2022 Contrast Security, Inc
|
|
3
|
+
* Contact: support@contrastsecurity.com
|
|
4
|
+
* License: Commercial
|
|
5
|
+
|
|
6
|
+
* NOTICE: This Software and the patented inventions embodied within may only be
|
|
7
|
+
* used as part of Contrast Security’s commercial offerings. Even though it is
|
|
8
|
+
* made available through public repositories, use of this Software is subject to
|
|
9
|
+
* the applicable End User Licensing Agreement found at
|
|
10
|
+
* https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
|
|
11
|
+
* between Contrast Security and the End User. The Software may not be reverse
|
|
12
|
+
* engineered, modified, repackaged, sold, redistributed or otherwise used in a
|
|
13
|
+
* way not consistent with the End User License Agreement.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const { InputType } = require('@contrast/common');
|
|
19
|
+
const { patchType } = require('../../../propagation/common');
|
|
20
|
+
|
|
21
|
+
module.exports = function init(core) {
|
|
22
|
+
const { depHooks, patcher, logger } = core;
|
|
23
|
+
|
|
24
|
+
core.assess.dataflow.sources.expressInstrumentation.params = {
|
|
25
|
+
install() {
|
|
26
|
+
const name = 'Layer.prototype.match';
|
|
27
|
+
depHooks.resolve(
|
|
28
|
+
{ name: 'express', file: 'lib/router/layer.js' },
|
|
29
|
+
(Layer) => {
|
|
30
|
+
patcher.patch(Layer.prototype, 'match', {
|
|
31
|
+
name,
|
|
32
|
+
patchType,
|
|
33
|
+
post(data) {
|
|
34
|
+
const layer = data.obj;
|
|
35
|
+
|
|
36
|
+
// we can exit early if
|
|
37
|
+
// the layer doesn't match the request or
|
|
38
|
+
// the layer doesn't recognize any parameters
|
|
39
|
+
if (!data.result || !layer.keys || layer.keys.length === 0) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const sourceContext = core.scopes.sources.getStore()?.assess;
|
|
44
|
+
|
|
45
|
+
if (!sourceContext) {
|
|
46
|
+
logger.error({ name }, 'unable to handle source. Missing `sourceContext`');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (sourceContext.parsedParams) {
|
|
51
|
+
logger.trace({ name }, 'values already tracked');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
core.assess.dataflow.sources.handle({
|
|
57
|
+
context: 'req.params',
|
|
58
|
+
name,
|
|
59
|
+
inputType: InputType.PARAMETER_VALUE,
|
|
60
|
+
stacktraceOpts: {
|
|
61
|
+
constructorOpt: data.hooked,
|
|
62
|
+
prependFrames: [data.orig]
|
|
63
|
+
},
|
|
64
|
+
data: layer.params,
|
|
65
|
+
sourceContext
|
|
66
|
+
});
|
|
67
|
+
sourceContext.parsedParams = true;
|
|
68
|
+
} catch (err) {
|
|
69
|
+
logger.error({ err, name }, 'unable to handle source');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return Layer;
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
return core.assess.dataflow.sources.expressInstrumentation.params;
|
|
81
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright: 2022 Contrast Security, Inc
|
|
3
|
+
* Contact: support@contrastsecurity.com
|
|
4
|
+
* License: Commercial
|
|
5
|
+
|
|
6
|
+
* NOTICE: This Software and the patented inventions embodied within may only be
|
|
7
|
+
* used as part of Contrast Security’s commercial offerings. Even though it is
|
|
8
|
+
* made available through public repositories, use of this Software is subject to
|
|
9
|
+
* the applicable End User Licensing Agreement found at
|
|
10
|
+
* https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
|
|
11
|
+
* between Contrast Security and the End User. The Software may not be reverse
|
|
12
|
+
* engineered, modified, repackaged, sold, redistributed or otherwise used in a
|
|
13
|
+
* way not consistent with the End User License Agreement.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const { InputType } = require('@contrast/common');
|
|
19
|
+
const { patchType } = require('../../common');
|
|
20
|
+
|
|
21
|
+
module.exports = function init(core) {
|
|
22
|
+
const {
|
|
23
|
+
assess: {
|
|
24
|
+
dataflow: { sources }
|
|
25
|
+
},
|
|
26
|
+
depHooks,
|
|
27
|
+
patcher,
|
|
28
|
+
scopes
|
|
29
|
+
} = core;
|
|
30
|
+
|
|
31
|
+
core.assess.dataflow.sources.expressInstrumentation.parsedUrl = {
|
|
32
|
+
install() {
|
|
33
|
+
depHooks.resolve(
|
|
34
|
+
{ name: 'express', file: 'lib/middleware/init.js' },
|
|
35
|
+
/** @param {import('express/lib/middleware/init')} mw */
|
|
36
|
+
(mw) => {
|
|
37
|
+
const name = 'express.middleware.init';
|
|
38
|
+
patcher.patch(mw, 'init', {
|
|
39
|
+
name,
|
|
40
|
+
patchType,
|
|
41
|
+
post(data) {
|
|
42
|
+
data.result = patcher.patch(data.result, {
|
|
43
|
+
name: 'express.middleware.init.expressInit',
|
|
44
|
+
patchType,
|
|
45
|
+
pre(data) {
|
|
46
|
+
const { args: [req] } = data;
|
|
47
|
+
patcher.patch(data.args, '2', {
|
|
48
|
+
name: 'express.middleware.init.expressInit.next',
|
|
49
|
+
patchType,
|
|
50
|
+
pre(data) {
|
|
51
|
+
const sourceContext = scopes.sources.getStore()?.assess;
|
|
52
|
+
if (!sourceContext) return;
|
|
53
|
+
|
|
54
|
+
const sourceInfo = {
|
|
55
|
+
context: 'req._parsedUrl',
|
|
56
|
+
data: req._parsedUrl,
|
|
57
|
+
name,
|
|
58
|
+
sourceContext,
|
|
59
|
+
stacktraceOpts: {
|
|
60
|
+
constructorOpt: data.hooked
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
sources.handle({
|
|
65
|
+
...sourceInfo,
|
|
66
|
+
inputType: InputType.URI,
|
|
67
|
+
keys: ['href', 'path', 'pathname'],
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
sources.handle({
|
|
71
|
+
...sourceInfo,
|
|
72
|
+
inputType: InputType.QUERYSTRING,
|
|
73
|
+
keys: ['query', 'search'],
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
return core.assess.dataflow.sources.expressInstrumentation.parsedUrl;
|
|
87
|
+
};
|
|
@@ -19,9 +19,9 @@ const { toLowerCase, InputType } = require('@contrast/common');
|
|
|
19
19
|
|
|
20
20
|
module.exports = function(core) {
|
|
21
21
|
const {
|
|
22
|
-
scopes
|
|
22
|
+
scopes,
|
|
23
23
|
instrumentation: { instrument },
|
|
24
|
-
assess: { dataflow
|
|
24
|
+
assess: { dataflow },
|
|
25
25
|
patcher,
|
|
26
26
|
} = core;
|
|
27
27
|
|
|
@@ -40,7 +40,7 @@ module.exports = function(core) {
|
|
|
40
40
|
|
|
41
41
|
try {
|
|
42
42
|
const [, req, res] = data.args;
|
|
43
|
-
const store = sources.getStore();
|
|
43
|
+
const store = scopes.sources.getStore();
|
|
44
44
|
|
|
45
45
|
if (!store) {
|
|
46
46
|
logger.debug('cannot acquire store for assess request handling');
|
|
@@ -80,7 +80,7 @@ module.exports = function(core) {
|
|
|
80
80
|
pre(data) {
|
|
81
81
|
const [name = '', value] = data.args;
|
|
82
82
|
if (toLowerCase(name) === 'content-type' && value) {
|
|
83
|
-
|
|
83
|
+
scopes.sources.getStore().assess.responseData.contentType = value;
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
});
|
|
@@ -97,7 +97,6 @@ module.exports = function(core) {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
const headers = {};
|
|
100
|
-
const sourceInputType = InputType.HEADER;
|
|
101
100
|
const sourceName = 'ClientRequest';
|
|
102
101
|
|
|
103
102
|
store.assess = {
|
|
@@ -107,22 +106,37 @@ module.exports = function(core) {
|
|
|
107
106
|
findings: {},
|
|
108
107
|
};
|
|
109
108
|
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
const sourceInfo = {
|
|
110
|
+
name: sourceName,
|
|
111
|
+
stacktraceOpts: {
|
|
112
|
+
constructorOpt: data.hooked,
|
|
113
|
+
prependFrames: [data.orig]
|
|
114
|
+
},
|
|
115
|
+
sourceContext: store.assess
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
[
|
|
119
|
+
{
|
|
112
120
|
context: 'req.headers',
|
|
121
|
+
inputType: InputType.HEADER,
|
|
113
122
|
data: req.headers,
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
123
|
+
...sourceInfo,
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
context: 'req',
|
|
127
|
+
keys: ['url'],
|
|
128
|
+
inputType: InputType.URI,
|
|
129
|
+
data: req,
|
|
130
|
+
...sourceInfo,
|
|
131
|
+
}
|
|
132
|
+
].forEach((sourceData) => {
|
|
133
|
+
const { inputType } = sourceData;
|
|
134
|
+
try {
|
|
135
|
+
dataflow.sources.handle(sourceData);
|
|
136
|
+
} catch (err) {
|
|
137
|
+
logger.error({ err, inputType, name: sourceName }, 'unable to handle http source');
|
|
138
|
+
}
|
|
139
|
+
});
|
|
126
140
|
|
|
127
141
|
for (let i = 0; i < req.rawHeaders.length; i += 2) {
|
|
128
142
|
const header = toLowerCase(req.rawHeaders[i]);
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright: 2022 Contrast Security, Inc
|
|
3
|
+
* Contact: support@contrastsecurity.com
|
|
4
|
+
* License: Commercial
|
|
5
|
+
|
|
6
|
+
* NOTICE: This Software and the patented inventions embodied within may only be
|
|
7
|
+
* used as part of Contrast Security’s commercial offerings. Even though it is
|
|
8
|
+
* made available through public repositories, use of this Software is subject to
|
|
9
|
+
* the applicable End User Licensing Agreement found at
|
|
10
|
+
* https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
|
|
11
|
+
* between Contrast Security and the End User. The Software may not be reverse
|
|
12
|
+
* engineered, modified, repackaged, sold, redistributed or otherwise used in a
|
|
13
|
+
* way not consistent with the End User License Agreement.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const { InputType } = require('@contrast/common');
|
|
19
|
+
const { patchType } = require('../common');
|
|
20
|
+
|
|
21
|
+
module.exports = (core) => {
|
|
22
|
+
const { depHooks, patcher, logger } = core;
|
|
23
|
+
|
|
24
|
+
core.assess.dataflow.sources.querystringInstrumentation = {
|
|
25
|
+
install() {
|
|
26
|
+
const name = 'querystring.parse';
|
|
27
|
+
depHooks.resolve({ name: 'querystring' },
|
|
28
|
+
(querystring) => patcher.patch(querystring, 'parse', {
|
|
29
|
+
name,
|
|
30
|
+
patchType,
|
|
31
|
+
post({ args, hooked, orig, result }) {
|
|
32
|
+
const sourceContext = core.scopes.sources.getStore()?.assess;
|
|
33
|
+
const inputType = InputType.QUERYSTRING;
|
|
34
|
+
|
|
35
|
+
if (!sourceContext) {
|
|
36
|
+
logger.error({ name }, 'unable to handle source. Missing `sourceContext`');
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (sourceContext.parsedQuery) {
|
|
41
|
+
logger.trace({ name }, 'values already tracked');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// We only run analysis for the `querystring` result when it's used
|
|
46
|
+
// as the framework's query parser
|
|
47
|
+
if (sourceContext.reqData?.queries === args[0]) {
|
|
48
|
+
try {
|
|
49
|
+
core.assess.dataflow.sources.handle({
|
|
50
|
+
context: 'req.query',
|
|
51
|
+
name,
|
|
52
|
+
inputType,
|
|
53
|
+
stacktraceOpts: {
|
|
54
|
+
constructorOpt: hooked,
|
|
55
|
+
prependFrames: [orig]
|
|
56
|
+
},
|
|
57
|
+
data: result,
|
|
58
|
+
sourceContext
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// we do not set the `parsedQuery` value here so that frameworks
|
|
62
|
+
// may handle queries in their own more specific manner.
|
|
63
|
+
} catch (err) {
|
|
64
|
+
logger.error({ err, name }, 'unable to handle source');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return core.assess.dataflow.sources.querystringInstrumentation;
|
|
74
|
+
};
|
|
75
|
+
|