@contrast/assess 1.6.0 → 1.8.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/propagation/index.js +3 -0
- package/lib/dataflow/propagation/install/JSON/index.js +33 -0
- package/lib/dataflow/propagation/install/JSON/stringify.js +290 -0
- package/lib/dataflow/propagation/install/buffer.js +79 -0
- package/lib/dataflow/propagation/install/contrast-methods/string.js +5 -1
- package/lib/dataflow/propagation/install/encode-uri-component.js +5 -2
- package/lib/dataflow/propagation/install/pug-runtime-escape.js +5 -2
- package/lib/dataflow/propagation/install/sequelize.js +310 -0
- package/lib/dataflow/propagation/install/sql-template-strings.js +5 -4
- package/lib/dataflow/propagation/install/string/match.js +2 -2
- package/lib/dataflow/propagation/install/string/replace.js +9 -4
- package/lib/dataflow/sinks/common.js +10 -1
- package/lib/dataflow/sinks/index.js +30 -1
- package/lib/dataflow/sinks/install/express/index.js +29 -0
- package/lib/dataflow/sinks/install/express/unvalidated-redirect.js +134 -0
- package/lib/dataflow/sinks/install/fastify/unvalidated-redirect.js +96 -69
- package/lib/dataflow/sinks/install/http.js +20 -5
- package/lib/dataflow/sinks/install/koa/unvalidated-redirect.js +33 -9
- package/lib/dataflow/sinks/install/mongodb.js +297 -82
- package/lib/dataflow/sinks/install/mssql.js +9 -4
- package/lib/dataflow/sinks/install/mysql.js +20 -4
- package/lib/dataflow/sinks/install/postgres.js +25 -12
- package/lib/dataflow/sinks/install/sequelize.js +142 -0
- package/lib/dataflow/sinks/install/sqlite3.js +9 -4
- package/lib/dataflow/sources/handler.js +144 -26
- package/lib/dataflow/sources/index.js +6 -8
- package/lib/dataflow/sources/install/body-parser1.js +133 -0
- package/lib/dataflow/sources/install/cookie-parser1.js +101 -0
- package/lib/dataflow/sources/install/express/index.js +31 -0
- 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 +32 -18
- package/lib/dataflow/sources/install/querystring.js +75 -0
- package/lib/dataflow/tag-utils.js +68 -1
- package/package.json +3 -3
|
@@ -0,0 +1,142 @@
|
|
|
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 util = require('util');
|
|
19
|
+
const {
|
|
20
|
+
Rule: { SQL_INJECTION },
|
|
21
|
+
DataflowTag: {
|
|
22
|
+
UNTRUSTED,
|
|
23
|
+
SQL_ENCODED,
|
|
24
|
+
LIMITED_CHARS,
|
|
25
|
+
CUSTOM_VALIDATED,
|
|
26
|
+
CUSTOM_ENCODED,
|
|
27
|
+
},
|
|
28
|
+
} = require('@contrast/common');
|
|
29
|
+
const { patchType, filterSafeTags } = require('../common');
|
|
30
|
+
|
|
31
|
+
module.exports = function (core) {
|
|
32
|
+
const {
|
|
33
|
+
depHooks,
|
|
34
|
+
patcher,
|
|
35
|
+
config,
|
|
36
|
+
scopes: { sources },
|
|
37
|
+
assess: {
|
|
38
|
+
dataflow: {
|
|
39
|
+
tracker,
|
|
40
|
+
sinks: { isVulnerable, runInActiveSink, reportFindings, reportSafePositive },
|
|
41
|
+
eventFactory: { createSinkEvent },
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
} = core;
|
|
45
|
+
|
|
46
|
+
const safeTags = [
|
|
47
|
+
SQL_ENCODED,
|
|
48
|
+
LIMITED_CHARS,
|
|
49
|
+
CUSTOM_VALIDATED,
|
|
50
|
+
CUSTOM_ENCODED
|
|
51
|
+
];
|
|
52
|
+
const requiredTag = UNTRUSTED;
|
|
53
|
+
const inspect = patcher.unwrap(util.inspect);
|
|
54
|
+
|
|
55
|
+
const sequelize = (core.assess.dataflow.sinks.sequelize = {});
|
|
56
|
+
|
|
57
|
+
sequelize.install = function () {
|
|
58
|
+
const sequelizeQueryPatchName = 'sequelize.prototype.query';
|
|
59
|
+
depHooks.resolve({ name: 'sequelize' }, (sequelize) => {
|
|
60
|
+
patcher.patch(sequelize.prototype, 'query', {
|
|
61
|
+
name: sequelizeQueryPatchName,
|
|
62
|
+
patchType,
|
|
63
|
+
around(next, data) {
|
|
64
|
+
const { args, hooked, orig } = data;
|
|
65
|
+
const sourceContext = sources.getStore()?.assess;
|
|
66
|
+
if (!sourceContext || !args[0]) return next();
|
|
67
|
+
|
|
68
|
+
const query = typeof args[0] === 'string' ? args[0] : args[0].query;
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const queryInfo = tracker.getData(query);
|
|
72
|
+
const isVulnerableQuery = isVulnerable(requiredTag, safeTags, queryInfo.tags);
|
|
73
|
+
|
|
74
|
+
if (queryInfo && !isVulnerableQuery && config.assess.safe_positives.enable) {
|
|
75
|
+
reportSafePositive({
|
|
76
|
+
name: sequelizeQueryPatchName,
|
|
77
|
+
ruleId: SQL_INJECTION,
|
|
78
|
+
safeTags: filterSafeTags(safeTags, queryInfo),
|
|
79
|
+
strInfo: {
|
|
80
|
+
value: queryInfo?.value,
|
|
81
|
+
tags: queryInfo.tags,
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (
|
|
87
|
+
!queryInfo ||
|
|
88
|
+
!isVulnerableQuery
|
|
89
|
+
) {
|
|
90
|
+
return runInActiveSink(SQL_INJECTION, async () => await next());
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const sqlValue =
|
|
94
|
+
typeof args[0] === 'string' ? args[0] : inspect(args[0]);
|
|
95
|
+
const inspectedOptions = args[1] ? inspect(args[1]) : '';
|
|
96
|
+
const contextArgs = args[1]
|
|
97
|
+
? `${sqlValue}, ${inspectedOptions}`
|
|
98
|
+
: sqlValue;
|
|
99
|
+
|
|
100
|
+
const reportedArgs = [{ value: sqlValue, tracked: true }];
|
|
101
|
+
args[1] &&
|
|
102
|
+
reportedArgs.push({ value: inspectedOptions, tracked: false });
|
|
103
|
+
|
|
104
|
+
const event = createSinkEvent({
|
|
105
|
+
context: `sequelize.prototype.query(${contextArgs})`,
|
|
106
|
+
name: sequelizeQueryPatchName,
|
|
107
|
+
history: [queryInfo],
|
|
108
|
+
object: {
|
|
109
|
+
value: 'sequelize.prototype',
|
|
110
|
+
tracked: false,
|
|
111
|
+
},
|
|
112
|
+
args: reportedArgs,
|
|
113
|
+
tags: queryInfo?.tags,
|
|
114
|
+
source: 'P0',
|
|
115
|
+
stacktraceOpts: {
|
|
116
|
+
contructorOpt: hooked,
|
|
117
|
+
prependFrames: [orig],
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
if (event) {
|
|
122
|
+
reportFindings({
|
|
123
|
+
ruleId: SQL_INJECTION,
|
|
124
|
+
sinkEvent: event,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
/* c8 ignore next 3 */
|
|
128
|
+
} catch (err) {
|
|
129
|
+
core.logger.error(
|
|
130
|
+
{ name: sequelizeQueryPatchName, err },
|
|
131
|
+
'assess sink analysis failed'
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return runInActiveSink(SQL_INJECTION, async () => await next());
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
return sequelize;
|
|
142
|
+
};
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
const { patchType } = require('../common');
|
|
19
19
|
const {
|
|
20
20
|
DataflowTag: { UNTRUSTED, SQL_ENCODED, LIMITED_CHARS, CUSTOM_VALIDATED, CUSTOM_ENCODED },
|
|
21
|
-
Rule,
|
|
21
|
+
Rule: { SQL_INJECTION },
|
|
22
22
|
isString
|
|
23
23
|
} = require('@contrast/common');
|
|
24
24
|
|
|
@@ -37,7 +37,7 @@ module.exports = function (core) {
|
|
|
37
37
|
assess: {
|
|
38
38
|
dataflow: {
|
|
39
39
|
tracker,
|
|
40
|
-
sinks: { isVulnerable, reportFindings },
|
|
40
|
+
sinks: { isVulnerable, isLocked, reportFindings },
|
|
41
41
|
eventFactory: { createSinkEvent },
|
|
42
42
|
},
|
|
43
43
|
},
|
|
@@ -45,7 +45,12 @@ module.exports = function (core) {
|
|
|
45
45
|
|
|
46
46
|
const pre = (name) => (data) => {
|
|
47
47
|
const store = sources.getStore()?.assess;
|
|
48
|
-
if (
|
|
48
|
+
if (
|
|
49
|
+
!store ||
|
|
50
|
+
!data.args[0] ||
|
|
51
|
+
!isString(data.args[0]) ||
|
|
52
|
+
isLocked(SQL_INJECTION)
|
|
53
|
+
) return;
|
|
49
54
|
|
|
50
55
|
const strInfo = tracker.getData(data.args[0]);
|
|
51
56
|
if (!strInfo || !isVulnerable(UNTRUSTED, safeTags, strInfo.tags)) {
|
|
@@ -74,7 +79,7 @@ module.exports = function (core) {
|
|
|
74
79
|
|
|
75
80
|
if (event) {
|
|
76
81
|
reportFindings({
|
|
77
|
-
ruleId:
|
|
82
|
+
ruleId: SQL_INJECTION,
|
|
78
83
|
sinkEvent: event,
|
|
79
84
|
});
|
|
80
85
|
}
|
|
@@ -19,7 +19,6 @@ const {
|
|
|
19
19
|
InputType,
|
|
20
20
|
DataflowTag,
|
|
21
21
|
isString,
|
|
22
|
-
traverseValues
|
|
23
22
|
} = require('@contrast/common');
|
|
24
23
|
|
|
25
24
|
module.exports = function(core) {
|
|
@@ -28,17 +27,17 @@ module.exports = function(core) {
|
|
|
28
27
|
dataflow: {
|
|
29
28
|
sources,
|
|
30
29
|
tracker,
|
|
31
|
-
eventFactory
|
|
30
|
+
eventFactory
|
|
32
31
|
}
|
|
33
32
|
},
|
|
34
|
-
createSnapshot,
|
|
35
33
|
config,
|
|
34
|
+
createSnapshot,
|
|
36
35
|
logger,
|
|
37
36
|
} = core;
|
|
38
37
|
|
|
39
38
|
const emptyStack = Object.freeze([]);
|
|
40
39
|
|
|
41
|
-
sources.createTags = function createTags({ inputType,
|
|
40
|
+
sources.createTags = function createTags({ inputType, fieldName = '', value }) {
|
|
42
41
|
if (!value?.length) {
|
|
43
42
|
return null;
|
|
44
43
|
}
|
|
@@ -48,55 +47,93 @@ module.exports = function(core) {
|
|
|
48
47
|
[DataflowTag.UNTRUSTED]: [0, stop]
|
|
49
48
|
};
|
|
50
49
|
|
|
51
|
-
if (inputType === InputType.HEADER &&
|
|
50
|
+
if (inputType === InputType.HEADER && fieldName.toLowerCase() === 'referer') {
|
|
52
51
|
tags[DataflowTag.HEADER] = [0, stop];
|
|
53
52
|
}
|
|
54
53
|
|
|
55
54
|
return tags;
|
|
56
55
|
};
|
|
57
56
|
|
|
57
|
+
sources.createStacktrace = function(stacktraceOpts) {
|
|
58
|
+
return config.assess.stacktraces === 'NONE'
|
|
59
|
+
? emptyStack
|
|
60
|
+
: createSnapshot(stacktraceOpts)();
|
|
61
|
+
};
|
|
62
|
+
|
|
58
63
|
sources.handle = function({
|
|
59
64
|
context,
|
|
65
|
+
keys,
|
|
60
66
|
name,
|
|
61
67
|
inputType = InputType.UNKNOWN,
|
|
62
68
|
stacktraceOpts,
|
|
63
69
|
data,
|
|
64
|
-
sourceContext
|
|
70
|
+
sourceContext,
|
|
65
71
|
}) {
|
|
66
72
|
if (!data) return;
|
|
67
73
|
|
|
68
|
-
const max = config.assess.max_context_source_events;
|
|
69
|
-
|
|
70
74
|
if (!sourceContext) {
|
|
71
75
|
core.logger.trace({ inputType, name }, 'skipping assess source handling - no request context');
|
|
72
76
|
return null;
|
|
73
77
|
}
|
|
74
78
|
|
|
79
|
+
if (!context) {
|
|
80
|
+
context = inputType;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const max = config.assess.max_context_source_events;
|
|
84
|
+
let _data = data;
|
|
75
85
|
let stack;
|
|
76
86
|
|
|
77
|
-
|
|
87
|
+
if (keys) {
|
|
88
|
+
_data = {};
|
|
89
|
+
for (const key of keys) {
|
|
90
|
+
_data[key] = data[key];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function createEvent({ fieldName, pathName, value }) {
|
|
95
|
+
// create the stacktrace once per call to .handle()
|
|
96
|
+
stack || (stack = sources.createStacktrace(stacktraceOpts));
|
|
97
|
+
return eventFactory.createSourceEvent({
|
|
98
|
+
context: `${context}.${pathName}`,
|
|
99
|
+
name,
|
|
100
|
+
fieldName,
|
|
101
|
+
pathName,
|
|
102
|
+
stack,
|
|
103
|
+
inputType,
|
|
104
|
+
tags: sources.createTags({ inputType, fieldName, value }),
|
|
105
|
+
result: { tracked: true, value },
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (Buffer.isBuffer(data) && !tracker.getData(data)) {
|
|
110
|
+
const event = createEvent({ pathName: 'body', value: data, fieldName: '' });
|
|
111
|
+
if (event) {
|
|
112
|
+
tracker.track(data, event);
|
|
113
|
+
}
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
traverse(_data, (path, fieldName, value, obj) => {
|
|
118
|
+
const pathName = path.join('.');
|
|
119
|
+
|
|
78
120
|
if (sourceContext.sourceEventsCount >= max) {
|
|
79
121
|
core.logger.trace({ inputType, name }, 'exiting assess source handling - %s max events exceeded', max);
|
|
80
122
|
return true;
|
|
81
123
|
}
|
|
82
124
|
|
|
83
125
|
if (isString(value) && value.length) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
pathName,
|
|
94
|
-
stack,
|
|
95
|
-
inputType,
|
|
96
|
-
tags: sources.createTags({ inputType, key, value }),
|
|
97
|
-
result: { tracked: true, value },
|
|
98
|
-
});
|
|
126
|
+
const strInfo = tracker.getData(value);
|
|
127
|
+
|
|
128
|
+
if (strInfo) {
|
|
129
|
+
// TODO: confirm this "layering-on" approach is what we want
|
|
130
|
+
// when the value is tracked the handler wins out and we "re-tracks" the value with new source
|
|
131
|
+
// event metadata. without this step tracker would complain about value already being tracked.
|
|
132
|
+
// alternatively we could treat this more like a propagation event and update existing metadata.
|
|
133
|
+
value = strInfo.value;
|
|
134
|
+
}
|
|
99
135
|
|
|
136
|
+
const event = createEvent({ pathName, value, fieldName });
|
|
100
137
|
if (!event) {
|
|
101
138
|
core.logger.warn({ inputType, name, pathName, value }, 'unable to create source event');
|
|
102
139
|
return;
|
|
@@ -104,15 +141,96 @@ module.exports = function(core) {
|
|
|
104
141
|
|
|
105
142
|
const { extern } = tracker.track(value, event);
|
|
106
143
|
if (extern) {
|
|
107
|
-
logger.trace({ extern,
|
|
108
|
-
obj[
|
|
144
|
+
logger.trace({ extern, fieldName, name, inputType }, 'tracked');
|
|
145
|
+
obj[fieldName] = extern;
|
|
146
|
+
|
|
109
147
|
sourceContext.sourceEventsCount++;
|
|
110
148
|
}
|
|
149
|
+
} else if (Buffer.isBuffer(value) && !tracker.getData(value)) {
|
|
150
|
+
const event = createEvent({ pathName, value, fieldName });
|
|
151
|
+
if (event) {
|
|
152
|
+
tracker.track(value, event);
|
|
153
|
+
} else {
|
|
154
|
+
core.logger.warn({ inputType, name, pathName, value }, 'unable to create source event');
|
|
155
|
+
}
|
|
111
156
|
}
|
|
112
157
|
});
|
|
113
158
|
|
|
159
|
+
if (keys) {
|
|
160
|
+
for (const key of keys) {
|
|
161
|
+
data[key] = _data[key];
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
114
165
|
return data;
|
|
115
166
|
};
|
|
116
167
|
|
|
117
168
|
return sources;
|
|
118
169
|
};
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* A custom traversal function for handling source value tracking efficiently.
|
|
173
|
+
* Implementation was adapted from traversal methods in @contrast/common.
|
|
174
|
+
* @param {any} target object to traverse
|
|
175
|
+
* @param {function} cb function<path, key, value, obj>
|
|
176
|
+
* @param {string[]} path path of node being visted; constructed of nested keys
|
|
177
|
+
* @param {boolean} halt whether to halt traversal; determined by callback
|
|
178
|
+
* @param {Set} visited used to dedupe circular references
|
|
179
|
+
*/
|
|
180
|
+
function traverse(target, cb, path = [], visited = new Set()) {
|
|
181
|
+
if (isTraversable(target)) {
|
|
182
|
+
for (const key in target) {
|
|
183
|
+
path.push(key);
|
|
184
|
+
|
|
185
|
+
const value = target[key];
|
|
186
|
+
|
|
187
|
+
if (visited.has(value)) {
|
|
188
|
+
path.pop();
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (isVisitable(value)) {
|
|
193
|
+
const halt = cb(path, key, value, target) === false;
|
|
194
|
+
if (halt) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (isTraversable(value)) {
|
|
200
|
+
visited.add(value);
|
|
201
|
+
traverse(value, cb, path, visited);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
path.pop();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Visit strings, buffers, basic objects and arrays.
|
|
211
|
+
* @param {any} value the value to check
|
|
212
|
+
* @returns {boolean}
|
|
213
|
+
*/
|
|
214
|
+
function isVisitable(value) {
|
|
215
|
+
if (!value) return false;
|
|
216
|
+
|
|
217
|
+
return value.constructor?.name === 'String' ||
|
|
218
|
+
value.constructor?.name === 'Buffer' ||
|
|
219
|
+
(typeof value === 'object' && !value.constructor);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* The criteria for traversal is a strict as possible. We only traverse plain
|
|
224
|
+
* objects and arrays and objects created via Object.create(null).
|
|
225
|
+
* @param {any} value the value to check
|
|
226
|
+
* @returns {boolean}
|
|
227
|
+
*/
|
|
228
|
+
function isTraversable(value) {
|
|
229
|
+
if (!value || typeof value !== 'object') return false;
|
|
230
|
+
|
|
231
|
+
return value.constructor?.name === 'Object' ||
|
|
232
|
+
value.constructor?.name === 'Array' ||
|
|
233
|
+
!value.constructor;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
module.exports.traverse = traverse;
|
|
@@ -17,23 +17,21 @@
|
|
|
17
17
|
|
|
18
18
|
const { callChildComponentMethodsSync } = require('@contrast/common');
|
|
19
19
|
|
|
20
|
-
module.exports = function(core) {
|
|
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
|
-
|
|
25
|
+
require('./install/express')(core);
|
|
30
26
|
require('./install/fastify')(core);
|
|
31
27
|
require('./install/koa')(core);
|
|
32
|
-
|
|
33
|
-
// libraries
|
|
28
|
+
require('./install/body-parser1')(core);
|
|
34
29
|
require('./install/busboy1')(core);
|
|
30
|
+
require('./install/cookie-parser1')(core);
|
|
35
31
|
require('./install/formidable1')(core);
|
|
32
|
+
require('./install/http')(core);
|
|
36
33
|
require('./install/qs6')(core);
|
|
34
|
+
require('./install/querystring')(core);
|
|
37
35
|
|
|
38
36
|
sources.install = function install() {
|
|
39
37
|
callChildComponentMethodsSync(sources, 'install');
|
|
@@ -0,0 +1,133 @@
|
|
|
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
|
+
const METHODS = ['json', 'raw', 'text', 'urlencoded'];
|
|
22
|
+
const INPUT_TYPES = {
|
|
23
|
+
'body-parser.json.jsonParser': InputType.JSON_VALUE,
|
|
24
|
+
'body-parser.raw.rawParser': InputType.BODY,
|
|
25
|
+
'body-parser.text.textParser': InputType.BODY,
|
|
26
|
+
'body-parser.urlencoded.urlencodedParser': InputType.PARAMETER_VALUE,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
module.exports = function init(core) {
|
|
30
|
+
const { assess, depHooks, logger, patcher, scopes } = core;
|
|
31
|
+
|
|
32
|
+
const createPreHook = (name) => (data) => {
|
|
33
|
+
const [req, , next] = data.args;
|
|
34
|
+
data.args[2] = function contrastNext(...args) {
|
|
35
|
+
const sourceContext = scopes.sources.getStore()?.assess;
|
|
36
|
+
|
|
37
|
+
if (!sourceContext) {
|
|
38
|
+
logger.error({ name }, 'unable to handle source. Missing `sourceContext`');
|
|
39
|
+
return next(...args);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (sourceContext.parsedBody) {
|
|
43
|
+
logger.trace({ name }, 'values already tracked');
|
|
44
|
+
return next(...args);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// when using a specific parser, we know the input type.
|
|
48
|
+
let inputType = INPUT_TYPES[name];
|
|
49
|
+
// when using `bodyParser()`, determine input type by content type.
|
|
50
|
+
if (!inputType) {
|
|
51
|
+
inputType = req.headers?.['content-type']?.includes('json')
|
|
52
|
+
? InputType.JSON_VALUE
|
|
53
|
+
: req.headers?.['content-type']?.includes('urlencoded')
|
|
54
|
+
? InputType.PARAMETER_VALUE
|
|
55
|
+
: InputType.BODY;
|
|
56
|
+
}
|
|
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
|
+
|
|
70
|
+
try {
|
|
71
|
+
assess.dataflow.sources.handle({
|
|
72
|
+
context,
|
|
73
|
+
data: _data,
|
|
74
|
+
name,
|
|
75
|
+
keys,
|
|
76
|
+
inputType,
|
|
77
|
+
sourceContext,
|
|
78
|
+
stacktraceOpts: {
|
|
79
|
+
constructorOpt: contrastNext
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
sourceContext.parsedBody = !!Object.keys(_data).length;
|
|
84
|
+
} catch (err) {
|
|
85
|
+
logger.error({ name, err }, 'unable to handle source');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return next(...args);
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
assess.dataflow.sources.bodyParser1Instrumentation = {
|
|
93
|
+
install() {
|
|
94
|
+
depHooks.resolve(
|
|
95
|
+
{ name: 'body-parser', version: '>=1.0.0' },
|
|
96
|
+
/** @param {import('body-parser').BodyParser} bodyParser */
|
|
97
|
+
(bodyParser) => {
|
|
98
|
+
bodyParser = patcher.patch(bodyParser, {
|
|
99
|
+
name: 'body-parser',
|
|
100
|
+
patchType,
|
|
101
|
+
post(data) {
|
|
102
|
+
const name = 'body-parser.bodyParser';
|
|
103
|
+
data.result = patcher.patch(data.result, {
|
|
104
|
+
name,
|
|
105
|
+
patchType,
|
|
106
|
+
pre: createPreHook(name),
|
|
107
|
+
});
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
METHODS.forEach((method) => {
|
|
112
|
+
patcher.patch(bodyParser, method, {
|
|
113
|
+
name: `body-parser.${method}`,
|
|
114
|
+
patchType,
|
|
115
|
+
post(data) {
|
|
116
|
+
const name = `body-parser.${method}.${method}Parser`;
|
|
117
|
+
data.result = patcher.patch(data.result, {
|
|
118
|
+
name,
|
|
119
|
+
patchType,
|
|
120
|
+
pre: createPreHook(name)
|
|
121
|
+
});
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return bodyParser;
|
|
127
|
+
}
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
return assess.dataflow.sources.bodyParser1Instrumentation;
|
|
133
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
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 { assess, depHooks, logger, patcher, scopes } = core;
|
|
23
|
+
|
|
24
|
+
assess.dataflow.sources.cookieParser1Instrumentation = {
|
|
25
|
+
install() {
|
|
26
|
+
depHooks.resolve(
|
|
27
|
+
{ name: 'cookie-parser', version: '>=1.0.0' },
|
|
28
|
+
/** @param {import('cookie-parser')} cookieParser */
|
|
29
|
+
(cookieParser) =>
|
|
30
|
+
patcher.patch(cookieParser, {
|
|
31
|
+
name: 'cookie-parser',
|
|
32
|
+
patchType,
|
|
33
|
+
post(data) {
|
|
34
|
+
const name = 'cookie-parser.cookieParser';
|
|
35
|
+
data.result = patcher.patch(data.result, {
|
|
36
|
+
name,
|
|
37
|
+
patchType,
|
|
38
|
+
pre(data) {
|
|
39
|
+
const [req, , next] = data.args;
|
|
40
|
+
data.args[2] = function contrastNext(...args) {
|
|
41
|
+
const sourceContext = scopes.sources.getStore()?.assess;
|
|
42
|
+
|
|
43
|
+
if (!sourceContext) {
|
|
44
|
+
logger.error({ name }, 'unable to handle source. Missing `sourceContext`');
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (sourceContext.parsedCookies) {
|
|
49
|
+
logger.trace({ name }, 'cookies already tracked');
|
|
50
|
+
} else if (req.cookies) {
|
|
51
|
+
try {
|
|
52
|
+
assess.dataflow.sources.handle({
|
|
53
|
+
context: 'req.cookies',
|
|
54
|
+
name,
|
|
55
|
+
inputType: InputType.COOKIE_VALUE,
|
|
56
|
+
stacktraceOpts: {
|
|
57
|
+
constructorOpt: contrastNext
|
|
58
|
+
},
|
|
59
|
+
data: req.cookies,
|
|
60
|
+
sourceContext,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
sourceContext.parsedCookies = !!Object.keys(req.cookies).length;
|
|
64
|
+
} catch (err) {
|
|
65
|
+
logger.error({ name, err }, 'unable to handle source');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (sourceContext.parsedSignedCookies) {
|
|
70
|
+
logger.trace({ name }, 'signedCookies already tracked');
|
|
71
|
+
} else if (req.signedCookies) {
|
|
72
|
+
try {
|
|
73
|
+
assess.dataflow.sources.handle({
|
|
74
|
+
context: 'req.signedCookies',
|
|
75
|
+
name,
|
|
76
|
+
inputType: InputType.COOKIE_VALUE,
|
|
77
|
+
stacktraceOpts: {
|
|
78
|
+
constructorOpt: contrastNext
|
|
79
|
+
},
|
|
80
|
+
data: req.signedCookies,
|
|
81
|
+
sourceContext,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
sourceContext.parsedSignedCookies = !!Object.keys(req.signedCookies).length;
|
|
85
|
+
} catch (err) {
|
|
86
|
+
logger.error({ name, err }, 'unable to handle source');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return next(...args);
|
|
91
|
+
};
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
})
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
return assess.dataflow.sources.cookieParser1Instrumentation;
|
|
101
|
+
};
|