@contrast/protect 1.57.0 → 1.59.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/error-handlers/install/express.js +3 -0
- package/lib/get-source-context.js +2 -0
- package/lib/index.js +12 -2
- package/lib/input-analysis/handlers.js +581 -578
- package/lib/input-analysis/index.js +1 -1
- package/lib/input-tracing/handlers/index.js +1 -1
- package/lib/input-tracing/install/fs.js +1 -1
- package/lib/make-source-context.js +7 -1
- package/package.json +10 -10
|
@@ -31,7 +31,7 @@ const {
|
|
|
31
31
|
StringPrototypeSplit,
|
|
32
32
|
}
|
|
33
33
|
} = require('@contrast/common');
|
|
34
|
-
|
|
34
|
+
const { Core } = require('@contrast/core/lib/ioc/core');
|
|
35
35
|
//
|
|
36
36
|
// these rules are not implemented by agent-lib, but are being considered for
|
|
37
37
|
// implementation:
|
|
@@ -95,678 +95,681 @@ const acceptedMethods = new Set([
|
|
|
95
95
|
'version-control',
|
|
96
96
|
]);
|
|
97
97
|
|
|
98
|
-
module.exports =
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
keyType: agentLib.InputType.ParameterKey, inputType: agentLib.InputType.ParameterValue
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
// all handlers will be invoked with two arguments:
|
|
117
|
-
// 1) sourceContext object containing:
|
|
118
|
-
// - reqData, the abstract request object containing only what is needed
|
|
119
|
-
// - protect, the protect context
|
|
120
|
-
// - rules, exclusions, virtual patches (TS data). what was in effect for this
|
|
121
|
-
// url *at the time the request was started*. these will not change.
|
|
122
|
-
// - block(), function that executes block for this specific request
|
|
123
|
-
// - findings{}, consolidated collection of findings returned by score functions
|
|
124
|
-
// and augmented with disposition and additional information as needed.
|
|
125
|
-
// 2) input or inputs specific to that handler
|
|
126
|
-
//
|
|
127
|
-
// exclusions
|
|
128
|
-
// - url exclusions are applied at the level above the connect handler, if no rules
|
|
129
|
-
// are applicable to a given URL then it's not processed in any way by handlers.
|
|
130
|
-
// - input exclusions are applied **after** analysis. The rationale is that checking
|
|
131
|
-
// inputs against rules 1) is very fast and 2) dramatically pares down the number
|
|
132
|
-
// of exclusion checks that need to be made.
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* handleConnect()
|
|
136
|
-
*
|
|
137
|
-
* handle the inputs that are available when the HTTP 'request' event is emitted.
|
|
138
|
-
*
|
|
139
|
-
* this should *always* be the first handler called; it does setup that other
|
|
140
|
-
* handlers require.
|
|
141
|
-
*
|
|
142
|
-
* the specific data that is available to be processed at this time:
|
|
143
|
-
* - URI
|
|
144
|
-
* - queries
|
|
145
|
-
* - url params
|
|
146
|
-
* - headers (should exclude cookies header)
|
|
147
|
-
* - cookies (special handling of the header)
|
|
148
|
-
*
|
|
149
|
-
* but it really only makes sense to process:
|
|
150
|
-
* - URI
|
|
151
|
-
* - headers except cookies
|
|
152
|
-
*
|
|
153
|
-
* why? because the query string will (probably) be interpreted by the framework using
|
|
154
|
-
* 'qs' and that could result in creating an array or an object instead of a simple text
|
|
155
|
-
* value. and the cookies need to be parsed into individual cookies by the framework so
|
|
156
|
-
* that our code doesn't have to guess at the format, encoding, and other permutations.
|
|
157
|
-
* and for url params - only the framework knows that a portion of the url is a param.
|
|
158
|
-
*
|
|
159
|
-
* if it is known that 'qs' is not being used, then passing the query string in the
|
|
160
|
-
* 'connectInputs' makes sense; a flag similar to 'contentType' can be set and it can be
|
|
161
|
-
* used later to avoid calling 'handleQueryParams()'
|
|
162
|
-
*
|
|
163
|
-
* @param {Object} sourceContext { reqData, protect } that will be supplied to
|
|
164
|
-
* all handlers and sinks for this request. It will always be supplied by the caller
|
|
165
|
-
* to a handler; the handler is not aware of the implementation.
|
|
166
|
-
* @param {Object} connectInputs each property is an input to be evaluated by this
|
|
167
|
-
* handler.
|
|
168
|
-
* @returns {undefined|[String]} undefined to permit else [mode, rule] to block.
|
|
169
|
-
*/
|
|
170
|
-
inputAnalysis.handleConnect = function handleConnect(sourceContext, connectInputs) {
|
|
171
|
-
const { policy: { rulesMask } } = sourceContext;
|
|
172
|
-
|
|
173
|
-
inputAnalysis.handleVirtualPatches(
|
|
174
|
-
sourceContext,
|
|
175
|
-
{ URLS: connectInputs.rawUrl, HEADERS: connectInputs.headers }
|
|
176
|
-
);
|
|
177
|
-
|
|
178
|
-
const findings = agentLib.scoreRequestConnect(rulesMask, connectInputs, preferWW);
|
|
179
|
-
let block = mergeFindings(sourceContext, findings);
|
|
180
|
-
|
|
181
|
-
if (!block) {
|
|
182
|
-
block = inputAnalysis.handleMethodTampering(sourceContext, connectInputs);
|
|
183
|
-
}
|
|
98
|
+
module.exports = Core.makeComponent({
|
|
99
|
+
name: 'protect.inputAnalysis.handlers',
|
|
100
|
+
factory(core) {
|
|
101
|
+
const {
|
|
102
|
+
logger,
|
|
103
|
+
protect: {
|
|
104
|
+
agentLib,
|
|
105
|
+
inputAnalysis,
|
|
106
|
+
},
|
|
107
|
+
config,
|
|
108
|
+
} = core;
|
|
109
|
+
|
|
110
|
+
const jsonInputTypes = {
|
|
111
|
+
keyType: agentLib.InputType.JsonKey, inputType: agentLib.InputType.JsonValue
|
|
112
|
+
};
|
|
184
113
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* handleQueryParams()
|
|
190
|
-
*
|
|
191
|
-
* If all values are strings there there is no need to re-evaluate them; that was
|
|
192
|
-
* done on requestConnect. But some frameworks use packages like 'qs' to parse the
|
|
193
|
-
* search params, which is non-standard and has many options including changing the
|
|
194
|
-
* delimiters and the ability to create the equivalent of JSON from the search params
|
|
195
|
-
* string. If any of the values are not strings then they need to be evaluated here;
|
|
196
|
-
* these results replace the results generated at requestConnect.
|
|
197
|
-
*
|
|
198
|
-
* If it is known that 'qs' is being used then it is better not to have passed the
|
|
199
|
-
* querystring to scoreRequestConnect().
|
|
200
|
-
*
|
|
201
|
-
* @param {Object} sourceContext
|
|
202
|
-
* @param {Object} queryParams pojo {key: value, ...} for all query params/search params
|
|
203
|
-
*/
|
|
204
|
-
inputAnalysis.handleQueryParams = function handleQueryParams(sourceContext, queryParams) {
|
|
205
|
-
if (sourceContext.analyzedQuery) return;
|
|
206
|
-
sourceContext.analyzedQuery = true;
|
|
207
|
-
|
|
208
|
-
if (typeof queryParams !== 'object') {
|
|
209
|
-
logger.debug({ queryParams }, 'handleQueryParams() called with non-object');
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
114
|
+
const parameterInputTypes = {
|
|
115
|
+
keyType: agentLib.InputType.ParameterKey, inputType: agentLib.InputType.ParameterValue
|
|
116
|
+
};
|
|
212
117
|
|
|
213
|
-
|
|
118
|
+
// all handlers will be invoked with two arguments:
|
|
119
|
+
// 1) sourceContext object containing:
|
|
120
|
+
// - reqData, the abstract request object containing only what is needed
|
|
121
|
+
// - protect, the protect context
|
|
122
|
+
// - rules, exclusions, virtual patches (TS data). what was in effect for this
|
|
123
|
+
// url *at the time the request was started*. these will not change.
|
|
124
|
+
// - block(), function that executes block for this specific request
|
|
125
|
+
// - findings{}, consolidated collection of findings returned by score functions
|
|
126
|
+
// and augmented with disposition and additional information as needed.
|
|
127
|
+
// 2) input or inputs specific to that handler
|
|
128
|
+
//
|
|
129
|
+
// exclusions
|
|
130
|
+
// - url exclusions are applied at the level above the connect handler, if no rules
|
|
131
|
+
// are applicable to a given URL then it's not processed in any way by handlers.
|
|
132
|
+
// - input exclusions are applied **after** analysis. The rationale is that checking
|
|
133
|
+
// inputs against rules 1) is very fast and 2) dramatically pares down the number
|
|
134
|
+
// of exclusion checks that need to be made.
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* handleConnect()
|
|
138
|
+
*
|
|
139
|
+
* handle the inputs that are available when the HTTP 'request' event is emitted.
|
|
140
|
+
*
|
|
141
|
+
* this should *always* be the first handler called; it does setup that other
|
|
142
|
+
* handlers require.
|
|
143
|
+
*
|
|
144
|
+
* the specific data that is available to be processed at this time:
|
|
145
|
+
* - URI
|
|
146
|
+
* - queries
|
|
147
|
+
* - url params
|
|
148
|
+
* - headers (should exclude cookies header)
|
|
149
|
+
* - cookies (special handling of the header)
|
|
150
|
+
*
|
|
151
|
+
* but it really only makes sense to process:
|
|
152
|
+
* - URI
|
|
153
|
+
* - headers except cookies
|
|
154
|
+
*
|
|
155
|
+
* why? because the query string will (probably) be interpreted by the framework using
|
|
156
|
+
* 'qs' and that could result in creating an array or an object instead of a simple text
|
|
157
|
+
* value. and the cookies need to be parsed into individual cookies by the framework so
|
|
158
|
+
* that our code doesn't have to guess at the format, encoding, and other permutations.
|
|
159
|
+
* and for url params - only the framework knows that a portion of the url is a param.
|
|
160
|
+
*
|
|
161
|
+
* if it is known that 'qs' is not being used, then passing the query string in the
|
|
162
|
+
* 'connectInputs' makes sense; a flag similar to 'contentType' can be set and it can be
|
|
163
|
+
* used later to avoid calling 'handleQueryParams()'
|
|
164
|
+
*
|
|
165
|
+
* @param {Object} sourceContext { reqData, protect } that will be supplied to
|
|
166
|
+
* all handlers and sinks for this request. It will always be supplied by the caller
|
|
167
|
+
* to a handler; the handler is not aware of the implementation.
|
|
168
|
+
* @param {Object} connectInputs each property is an input to be evaluated by this
|
|
169
|
+
* handler.
|
|
170
|
+
* @returns {undefined|[String]} undefined to permit else [mode, rule] to block.
|
|
171
|
+
*/
|
|
172
|
+
inputAnalysis.handleConnect = function handleConnect(sourceContext, connectInputs) {
|
|
173
|
+
const { policy: { rulesMask } } = sourceContext;
|
|
174
|
+
|
|
175
|
+
inputAnalysis.handleVirtualPatches(
|
|
176
|
+
sourceContext,
|
|
177
|
+
{ URLS: connectInputs.rawUrl, HEADERS: connectInputs.headers }
|
|
178
|
+
);
|
|
214
179
|
|
|
215
|
-
|
|
180
|
+
const findings = agentLib.scoreRequestConnect(rulesMask, connectInputs, preferWW);
|
|
181
|
+
let block = mergeFindings(sourceContext, findings);
|
|
216
182
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
};
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* handleUrlParams()
|
|
224
|
-
*
|
|
225
|
-
* Invoked when a framework emits URL params. It is similar to handling queryParams
|
|
226
|
-
* except no finding for URL params can be present.
|
|
227
|
-
*
|
|
228
|
-
* @param {Object} sourceContext
|
|
229
|
-
* @param {Object} urlParams pojo
|
|
230
|
-
*/
|
|
231
|
-
inputAnalysis.handleUrlParams = function (sourceContext, urlParams) {
|
|
232
|
-
if (sourceContext.analyzedUrlParams) return;
|
|
233
|
-
sourceContext.analyzedUrlParams = true;
|
|
234
|
-
|
|
235
|
-
if (typeof urlParams !== 'object') {
|
|
236
|
-
logger.debug({ urlParams }, 'handleUrlParams() called with non-object');
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
inputAnalysis.handleVirtualPatches(sourceContext, { PARAMETERS: urlParams });
|
|
183
|
+
if (!block) {
|
|
184
|
+
block = inputAnalysis.handleMethodTampering(sourceContext, connectInputs);
|
|
185
|
+
}
|
|
241
186
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const { UrlParameter } = agentLib.InputType;
|
|
187
|
+
return block;
|
|
188
|
+
};
|
|
245
189
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
190
|
+
/**
|
|
191
|
+
* handleQueryParams()
|
|
192
|
+
*
|
|
193
|
+
* If all values are strings there there is no need to re-evaluate them; that was
|
|
194
|
+
* done on requestConnect. But some frameworks use packages like 'qs' to parse the
|
|
195
|
+
* search params, which is non-standard and has many options including changing the
|
|
196
|
+
* delimiters and the ability to create the equivalent of JSON from the search params
|
|
197
|
+
* string. If any of the values are not strings then they need to be evaluated here;
|
|
198
|
+
* these results replace the results generated at requestConnect.
|
|
199
|
+
*
|
|
200
|
+
* If it is known that 'qs' is being used then it is better not to have passed the
|
|
201
|
+
* querystring to scoreRequestConnect().
|
|
202
|
+
*
|
|
203
|
+
* @param {Object} sourceContext
|
|
204
|
+
* @param {Object} queryParams pojo {key: value, ...} for all query params/search params
|
|
205
|
+
*/
|
|
206
|
+
inputAnalysis.handleQueryParams = function handleQueryParams(sourceContext, queryParams) {
|
|
207
|
+
if (sourceContext.analyzedQuery) return;
|
|
208
|
+
sourceContext.analyzedQuery = true;
|
|
209
|
+
|
|
210
|
+
if (typeof queryParams !== 'object') {
|
|
211
|
+
logger.debug({ queryParams }, 'handleQueryParams() called with non-object');
|
|
249
212
|
return;
|
|
250
213
|
}
|
|
251
214
|
|
|
252
|
-
|
|
215
|
+
inputAnalysis.handleVirtualPatches(sourceContext, { PARAMETERS: queryParams });
|
|
253
216
|
|
|
254
|
-
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
217
|
+
const block = commonObjectAnalyzer(sourceContext, queryParams, parameterInputTypes);
|
|
257
218
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
ruleId: item.ruleId,
|
|
261
|
-
inputType: 'UrlParameter',
|
|
262
|
-
path: ArrayPrototypeSlice.call(path),
|
|
263
|
-
key: path.pop(), // there should always be at least the param name
|
|
264
|
-
value,
|
|
265
|
-
score: item.score,
|
|
266
|
-
idsList: item.idsList
|
|
267
|
-
});
|
|
219
|
+
if (block) {
|
|
220
|
+
core.protect.throwSecurityException(sourceContext);
|
|
268
221
|
}
|
|
269
|
-
}
|
|
222
|
+
};
|
|
270
223
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
224
|
+
/**
|
|
225
|
+
* handleUrlParams()
|
|
226
|
+
*
|
|
227
|
+
* Invoked when a framework emits URL params. It is similar to handling queryParams
|
|
228
|
+
* except no finding for URL params can be present.
|
|
229
|
+
*
|
|
230
|
+
* @param {Object} sourceContext
|
|
231
|
+
* @param {Object} urlParams pojo
|
|
232
|
+
*/
|
|
233
|
+
inputAnalysis.handleUrlParams = function(sourceContext, urlParams) {
|
|
234
|
+
if (sourceContext.analyzedUrlParams) return;
|
|
235
|
+
sourceContext.analyzedUrlParams = true;
|
|
236
|
+
|
|
237
|
+
if (typeof urlParams !== 'object') {
|
|
238
|
+
logger.debug({ urlParams }, 'handleUrlParams() called with non-object');
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
275
241
|
|
|
276
|
-
|
|
277
|
-
const urlParamsFindings = {
|
|
278
|
-
trackRequest: true,
|
|
279
|
-
securityException: undefined,
|
|
280
|
-
resultsList,
|
|
281
|
-
};
|
|
242
|
+
inputAnalysis.handleVirtualPatches(sourceContext, { PARAMETERS: urlParams });
|
|
282
243
|
|
|
283
|
-
|
|
244
|
+
const { policy: { rulesMask } } = sourceContext;
|
|
245
|
+
const resultsList = [];
|
|
246
|
+
const { UrlParameter } = agentLib.InputType;
|
|
284
247
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
248
|
+
traverseValues(urlParams, function(path, type, value) {
|
|
249
|
+
// url param names are not checked.
|
|
250
|
+
if (type !== 'Value') {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
289
253
|
|
|
290
|
-
|
|
291
|
-
* handleCookies()
|
|
292
|
-
*
|
|
293
|
-
* Invoked when a framework emits cookies. It is similar to handling queryParams except
|
|
294
|
-
* no findings for cookies can be present.
|
|
295
|
-
* @param {Object} sourceContext
|
|
296
|
-
* @param {Object} cookies pojo
|
|
297
|
-
*/
|
|
298
|
-
inputAnalysis.handleCookies = function (sourceContext, cookies) {
|
|
299
|
-
if (sourceContext.analyzedCookies) return;
|
|
300
|
-
sourceContext.analyzedCookies = true;
|
|
254
|
+
const items = agentLib.scoreAtom(rulesMask, value, UrlParameter, preferWW);
|
|
301
255
|
|
|
302
|
-
|
|
256
|
+
if (!items) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
303
259
|
|
|
304
|
-
|
|
260
|
+
for (const item of items) {
|
|
261
|
+
resultsList.push({
|
|
262
|
+
ruleId: item.ruleId,
|
|
263
|
+
inputType: 'UrlParameter',
|
|
264
|
+
path: ArrayPrototypeSlice.call(path),
|
|
265
|
+
key: path.pop(), // there should always be at least the param name
|
|
266
|
+
value,
|
|
267
|
+
score: item.score,
|
|
268
|
+
idsList: item.idsList
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
});
|
|
305
272
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}, []);
|
|
273
|
+
// if nothing was found then nothing needs to be done.
|
|
274
|
+
if (resultsList.length === 0) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
311
277
|
|
|
312
|
-
|
|
278
|
+
// something was found, so create "complete" findings.
|
|
279
|
+
const urlParamsFindings = {
|
|
280
|
+
trackRequest: true,
|
|
281
|
+
securityException: undefined,
|
|
282
|
+
resultsList,
|
|
283
|
+
};
|
|
313
284
|
|
|
314
|
-
|
|
285
|
+
const block = mergeFindings(sourceContext, urlParamsFindings);
|
|
315
286
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
/**
|
|
322
|
-
* handleParsedBody() is called with the body after a framework has parsed
|
|
323
|
-
* the body. If the body was JSON then it may have already been scored by
|
|
324
|
-
* handleRawBody(); if it was, bodyType was set to 'json' and this code only
|
|
325
|
-
* converts the previous findings, using the parsed body.
|
|
326
|
-
*
|
|
327
|
-
* @param {Object} sourceContext
|
|
328
|
-
* @param {Object} parsedBody
|
|
329
|
-
*/
|
|
330
|
-
inputAnalysis.handleParsedBody = function (sourceContext, parsedBody) {
|
|
331
|
-
if (sourceContext.analyzedBody) return;
|
|
332
|
-
|
|
333
|
-
sourceContext.analyzedBody = true;
|
|
334
|
-
|
|
335
|
-
if (typeof parsedBody !== 'object') {
|
|
336
|
-
logger.debug({ parsedBody }, 'handleParsedBody() called with non-object');
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
287
|
+
if (block) {
|
|
288
|
+
core.protect.throwSecurityException(sourceContext);
|
|
289
|
+
}
|
|
290
|
+
};
|
|
339
291
|
|
|
340
|
-
|
|
292
|
+
/**
|
|
293
|
+
* handleCookies()
|
|
294
|
+
*
|
|
295
|
+
* Invoked when a framework emits cookies. It is similar to handling queryParams except
|
|
296
|
+
* no findings for cookies can be present.
|
|
297
|
+
* @param {Object} sourceContext
|
|
298
|
+
* @param {Object} cookies pojo
|
|
299
|
+
*/
|
|
300
|
+
inputAnalysis.handleCookies = function(sourceContext, cookies) {
|
|
301
|
+
if (sourceContext.analyzedCookies) return;
|
|
302
|
+
sourceContext.analyzedCookies = true;
|
|
341
303
|
|
|
342
|
-
|
|
343
|
-
let inputTypes;
|
|
344
|
-
if (sourceContext.reqData.contentType.includes('/json')) {
|
|
345
|
-
bodyType = 'json';
|
|
346
|
-
inputTypes = jsonInputTypes;
|
|
347
|
-
} else {
|
|
348
|
-
bodyType = 'urlencoded';
|
|
349
|
-
inputTypes = parameterInputTypes;
|
|
350
|
-
}
|
|
304
|
+
inputAnalysis.handleVirtualPatches(sourceContext, { HEADERS: cookies });
|
|
351
305
|
|
|
352
|
-
|
|
306
|
+
const { policy: { rulesMask } } = sourceContext;
|
|
353
307
|
|
|
354
|
-
|
|
308
|
+
const cookiesArr = Object.entries(cookies).reduce((acc, [key, value]) => {
|
|
309
|
+
// things like booleans will cause agent-lib to throw
|
|
310
|
+
if (isString(value)) acc.push(key, value);
|
|
311
|
+
return acc;
|
|
312
|
+
}, []);
|
|
355
313
|
|
|
356
|
-
|
|
357
|
-
core.protect.throwSecurityException(sourceContext);
|
|
358
|
-
}
|
|
359
|
-
};
|
|
314
|
+
const cookieFindings = agentLib.scoreRequestConnect(rulesMask, { cookies: cookiesArr }, preferWW);
|
|
360
315
|
|
|
361
|
-
|
|
362
|
-
// of a dumb rule anyway. but maybe some code actually uses the name provided.
|
|
363
|
-
inputAnalysis.handleFileUploadName = function (sourceContext, names) {
|
|
364
|
-
const type = agentLib.InputType.MultipartName;
|
|
365
|
-
const { policy } = sourceContext;
|
|
366
|
-
const resultsList = [];
|
|
316
|
+
const block = mergeFindings(sourceContext, cookieFindings);
|
|
367
317
|
|
|
368
|
-
|
|
318
|
+
if (block) {
|
|
319
|
+
core.protect.throwSecurityException(sourceContext);
|
|
320
|
+
}
|
|
321
|
+
};
|
|
369
322
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
323
|
+
/**
|
|
324
|
+
* handleParsedBody() is called with the body after a framework has parsed
|
|
325
|
+
* the body. If the body was JSON then it may have already been scored by
|
|
326
|
+
* handleRawBody(); if it was, bodyType was set to 'json' and this code only
|
|
327
|
+
* converts the previous findings, using the parsed body.
|
|
328
|
+
*
|
|
329
|
+
* @param {Object} sourceContext
|
|
330
|
+
* @param {Object} parsedBody
|
|
331
|
+
*/
|
|
332
|
+
inputAnalysis.handleParsedBody = function(sourceContext, parsedBody) {
|
|
333
|
+
if (sourceContext.analyzedBody) return;
|
|
334
|
+
|
|
335
|
+
sourceContext.analyzedBody = true;
|
|
336
|
+
|
|
337
|
+
if (typeof parsedBody !== 'object') {
|
|
338
|
+
logger.debug({ parsedBody }, 'handleParsedBody() called with non-object');
|
|
373
339
|
return;
|
|
374
340
|
}
|
|
375
341
|
|
|
376
|
-
|
|
342
|
+
inputAnalysis.handleVirtualPatches(sourceContext, { PARAMETERS: parsedBody });
|
|
377
343
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
value: name,
|
|
387
|
-
score: item.score,
|
|
388
|
-
idsList: item.idsList,
|
|
389
|
-
});
|
|
344
|
+
let bodyType;
|
|
345
|
+
let inputTypes;
|
|
346
|
+
if (sourceContext.reqData.contentType.includes('/json')) {
|
|
347
|
+
bodyType = 'json';
|
|
348
|
+
inputTypes = jsonInputTypes;
|
|
349
|
+
} else {
|
|
350
|
+
bodyType = 'urlencoded';
|
|
351
|
+
inputTypes = parameterInputTypes;
|
|
390
352
|
}
|
|
391
|
-
}
|
|
392
353
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
354
|
+
const block = commonObjectAnalyzer(sourceContext, parsedBody, inputTypes);
|
|
355
|
+
|
|
356
|
+
sourceContext.bodyType = bodyType;
|
|
357
|
+
|
|
358
|
+
if (block) {
|
|
359
|
+
core.protect.throwSecurityException(sourceContext);
|
|
360
|
+
}
|
|
398
361
|
};
|
|
399
362
|
|
|
400
|
-
|
|
363
|
+
// was MULTIPART_NAME but maybe we should just call it what it is. it's kind
|
|
364
|
+
// of a dumb rule anyway. but maybe some code actually uses the name provided.
|
|
365
|
+
inputAnalysis.handleFileUploadName = function(sourceContext, names) {
|
|
366
|
+
const type = agentLib.InputType.MultipartName;
|
|
367
|
+
const { policy } = sourceContext;
|
|
368
|
+
const resultsList = [];
|
|
401
369
|
|
|
402
|
-
|
|
403
|
-
core.protect.throwSecurityException(sourceContext);
|
|
404
|
-
}
|
|
405
|
-
};
|
|
370
|
+
if (policy[Rule.UNSAFE_FILE_UPLOAD] === 'off') return;
|
|
406
371
|
|
|
407
|
-
|
|
408
|
-
|
|
372
|
+
for (const name of names) {
|
|
373
|
+
if (!isString(name)) {
|
|
374
|
+
logger.debug({ filename: name }, 'handleFileUploadName() was called with non-string');
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
409
377
|
|
|
410
|
-
|
|
378
|
+
const items = agentLib.scoreAtom(policy.rulesMask, name, type);
|
|
411
379
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
380
|
+
if (!items) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
for (const item of items) {
|
|
384
|
+
resultsList.push({
|
|
385
|
+
ruleId: item.ruleId,
|
|
386
|
+
inputType: type,
|
|
387
|
+
name: '',
|
|
388
|
+
value: name,
|
|
389
|
+
score: item.score,
|
|
390
|
+
idsList: item.idsList,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// something was found, so create "complete" findings.
|
|
396
|
+
const unsafeFilenameFindings = {
|
|
397
|
+
trackRequest: true,
|
|
398
|
+
securityException: undefined,
|
|
399
|
+
resultsList,
|
|
400
|
+
};
|
|
415
401
|
|
|
416
|
-
|
|
417
|
-
vpEvaluators.delete(key);
|
|
418
|
-
const { name, uuid } = vpEvaluators.get('metadata');
|
|
402
|
+
const block = mergeFindings(sourceContext, unsafeFilenameFindings);
|
|
419
403
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
404
|
+
if (block) {
|
|
405
|
+
core.protect.throwSecurityException(sourceContext);
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
inputAnalysis.handleVirtualPatches = function(sourceContext, requestInput) {
|
|
410
|
+
const ruleId = Rule.VIRTUAL_PATCH;
|
|
411
|
+
|
|
412
|
+
if (!Object.keys(requestInput).filter(Boolean).length || !sourceContext?.virtualPatchesEvaluators.length) return;
|
|
413
|
+
|
|
414
|
+
for (const vpEvaluators of sourceContext.virtualPatchesEvaluators) {
|
|
415
|
+
for (const key in requestInput) {
|
|
416
|
+
const evaluator = vpEvaluators.get(key);
|
|
417
|
+
|
|
418
|
+
if (evaluator && requestInput[key] && evaluator(requestInput[key])) {
|
|
419
|
+
vpEvaluators.delete(key);
|
|
420
|
+
const { name, uuid } = vpEvaluators.get('metadata');
|
|
421
|
+
|
|
422
|
+
if (vpEvaluators.size === 1 && uuid) {
|
|
423
|
+
if (!sourceContext.resultsMap[ruleId]) {
|
|
424
|
+
sourceContext.resultsMap[ruleId] = [];
|
|
425
|
+
}
|
|
426
|
+
sourceContext.resultsMap[ruleId].push({
|
|
427
|
+
name,
|
|
428
|
+
uuid
|
|
429
|
+
});
|
|
430
|
+
sourceContext.securityException = ['block', ruleId];
|
|
431
|
+
core.protect.throwSecurityException(sourceContext);
|
|
423
432
|
}
|
|
424
|
-
sourceContext.resultsMap[ruleId].push({
|
|
425
|
-
name,
|
|
426
|
-
uuid
|
|
427
|
-
});
|
|
428
|
-
sourceContext.securityException = ['block', ruleId];
|
|
429
|
-
core.protect.throwSecurityException(sourceContext);
|
|
430
433
|
}
|
|
431
434
|
}
|
|
432
435
|
}
|
|
433
|
-
}
|
|
434
|
-
};
|
|
436
|
+
};
|
|
435
437
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
+
inputAnalysis.handleIpAllowlist = function(sourceContext, ipAllowlist) {
|
|
439
|
+
if (!sourceContext || !ipAllowlist.length) return;
|
|
438
440
|
|
|
439
|
-
|
|
441
|
+
const { ip: reqIp, headers: reqHeaders } = sourceContext.reqData;
|
|
440
442
|
|
|
441
|
-
|
|
443
|
+
const match = ipListAnalysis(reqIp, reqHeaders, ipAllowlist);
|
|
442
444
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
445
|
+
if (match) {
|
|
446
|
+
logger.info(match, 'Found a matching IP to an entry in ipAllow list');
|
|
447
|
+
return true;
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
inputAnalysis.handleIpDenylist = function(sourceContext, ipDenylist) {
|
|
452
|
+
const ruleId = Rule.IP_DENYLIST;
|
|
448
453
|
|
|
449
|
-
|
|
450
|
-
const ruleId = Rule.IP_DENYLIST;
|
|
454
|
+
if (!sourceContext || !ipDenylist.length) return;
|
|
451
455
|
|
|
452
|
-
|
|
456
|
+
const { ip: reqIp, headers: reqHeaders } = sourceContext.reqData;
|
|
453
457
|
|
|
454
|
-
|
|
458
|
+
const match = ipListAnalysis(reqIp, reqHeaders, ipDenylist);
|
|
455
459
|
|
|
456
|
-
|
|
460
|
+
if (match) {
|
|
461
|
+
logger.info(match, 'Found a matching IP to an entry in ipDeny list');
|
|
462
|
+
if (!sourceContext.resultsMap[ruleId]) {
|
|
463
|
+
sourceContext.resultsMap[ruleId] = [];
|
|
464
|
+
}
|
|
457
465
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
466
|
+
sourceContext.resultsMap[ruleId].push({
|
|
467
|
+
ip: match.matchedIp,
|
|
468
|
+
uuid: match.uuid,
|
|
469
|
+
});
|
|
470
|
+
return ['block', 'ip-denylist'];
|
|
462
471
|
}
|
|
472
|
+
};
|
|
463
473
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
};
|
|
486
|
-
|
|
487
|
-
sourceContext.resultsMap[ruleId] = [result];
|
|
488
|
-
|
|
489
|
-
if (BLOCKING_MODES.includes(mode)) {
|
|
490
|
-
result.blocked = true;
|
|
491
|
-
return sourceContext.securityException = ['block', ruleId];
|
|
474
|
+
inputAnalysis.handleMethodTampering = function(sourceContext, connectInputs) {
|
|
475
|
+
const ruleId = Rule.METHOD_TAMPERING;
|
|
476
|
+
const mode = sourceContext.policy[ruleId];
|
|
477
|
+
if (mode !== OFF) {
|
|
478
|
+
const { method } = connectInputs;
|
|
479
|
+
|
|
480
|
+
if (!acceptedMethods.has(method)) {
|
|
481
|
+
const result = {
|
|
482
|
+
inputType: InputType.METHOD,
|
|
483
|
+
key: 'method',
|
|
484
|
+
value: method,
|
|
485
|
+
blocked: false,
|
|
486
|
+
exploitMetadata: null,
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
sourceContext.resultsMap[ruleId] = [result];
|
|
490
|
+
|
|
491
|
+
if (BLOCKING_MODES.includes(mode)) {
|
|
492
|
+
result.blocked = true;
|
|
493
|
+
return sourceContext.securityException = ['block', ruleId];
|
|
494
|
+
}
|
|
492
495
|
}
|
|
493
496
|
}
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
*
|
|
501
|
+
* Invoked when the request is complete. Handles probe analysis (when configured) and
|
|
502
|
+
* various other tasks needed prior to reporting.
|
|
503
|
+
*
|
|
504
|
+
* @param {Object} sourceContext
|
|
505
|
+
*/
|
|
506
|
+
inputAnalysis.handleRequestEnd = function handleRequestEnd(sourceContext) {
|
|
507
|
+
{
|
|
508
|
+
// check status code to verify method-tampering exploitation
|
|
509
|
+
const mtResult = sourceContext.resultsMap[Rule.METHOD_TAMPERING]?.[0];
|
|
510
|
+
if (mtResult) {
|
|
511
|
+
const { statusCode } = sourceContext.resData;
|
|
512
|
+
if (statusCode !== 405 || statusCode !== 501) {
|
|
513
|
+
mtResult.exploitMetadata = [{ statusCode }];
|
|
514
|
+
}
|
|
512
515
|
}
|
|
513
516
|
}
|
|
514
|
-
}
|
|
515
517
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
518
|
+
if (!config.protect.probe_analysis.enable) return;
|
|
519
|
+
|
|
520
|
+
// Detecting probes
|
|
521
|
+
const { resultsMap, policy: { rulesMask } } = sourceContext;
|
|
522
|
+
const probesRules = [Rule.CMD_INJECTION, Rule.PATH_TRAVERSAL, Rule.SQL_INJECTION, Rule.XXE];
|
|
523
|
+
const probes = {};
|
|
524
|
+
|
|
525
|
+
const findingsForScoreRequest = {
|
|
526
|
+
HeaderValue: {},
|
|
527
|
+
ParameterValue: {},
|
|
528
|
+
CookieValue: {},
|
|
529
|
+
};
|
|
530
|
+
const findingsForScoreAtom = {};
|
|
531
|
+
const valueToResultByRuleId = {};
|
|
532
|
+
|
|
533
|
+
Object.values(resultsMap).forEach(resultsByRuleId => {
|
|
534
|
+
resultsByRuleId.forEach(resultByRuleId => {
|
|
535
|
+
const {
|
|
536
|
+
ruleId,
|
|
537
|
+
exploitMetadata,
|
|
538
|
+
score,
|
|
539
|
+
value,
|
|
540
|
+
key,
|
|
541
|
+
inputType
|
|
542
|
+
} = resultByRuleId;
|
|
543
|
+
|
|
544
|
+
if (
|
|
545
|
+
!isMonitorMode(ruleId, sourceContext) ||
|
|
546
|
+
exploitMetadata.length > 0 ||
|
|
547
|
+
score >= 90 ||
|
|
548
|
+
!probesRules.some((rule) => rule === ruleId)
|
|
549
|
+
) {
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
522
552
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
const findingsForScoreAtom = {};
|
|
529
|
-
const valueToResultByRuleId = {};
|
|
530
|
-
|
|
531
|
-
Object.values(resultsMap).forEach(resultsByRuleId => {
|
|
532
|
-
resultsByRuleId.forEach(resultByRuleId => {
|
|
533
|
-
const {
|
|
534
|
-
ruleId,
|
|
535
|
-
exploitMetadata,
|
|
536
|
-
score,
|
|
537
|
-
value,
|
|
538
|
-
key,
|
|
539
|
-
inputType
|
|
540
|
-
} = resultByRuleId;
|
|
541
|
-
|
|
542
|
-
if (
|
|
543
|
-
!isMonitorMode(ruleId, sourceContext) ||
|
|
544
|
-
exploitMetadata.length > 0 ||
|
|
545
|
-
score >= 90 ||
|
|
546
|
-
!probesRules.some((rule) => rule === ruleId)
|
|
547
|
-
) {
|
|
548
|
-
return;
|
|
549
|
-
}
|
|
553
|
+
const dataType = findingsForScoreRequest[inputType];
|
|
554
|
+
if (!dataType) {
|
|
555
|
+
if (!findingsForScoreAtom[value]) {
|
|
556
|
+
findingsForScoreAtom[value] = {};
|
|
557
|
+
}
|
|
550
558
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
if (!findingsForScoreAtom[value]) {
|
|
554
|
-
findingsForScoreAtom[value] = {};
|
|
559
|
+
findingsForScoreAtom[value][inputType] = resultByRuleId;
|
|
560
|
+
return;
|
|
555
561
|
}
|
|
556
562
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
dataType[key] = value;
|
|
562
|
-
valueToResultByRuleId[value] = resultByRuleId;
|
|
563
|
+
dataType[key] = value;
|
|
564
|
+
valueToResultByRuleId[value] = resultByRuleId;
|
|
565
|
+
});
|
|
563
566
|
});
|
|
564
|
-
});
|
|
565
|
-
|
|
566
|
-
const { ParameterValue, HeaderValue, CookieValue } = findingsForScoreRequest;
|
|
567
|
-
|
|
568
|
-
const results =
|
|
569
|
-
agentLib.scoreRequestConnect(
|
|
570
|
-
rulesMask,
|
|
571
|
-
{
|
|
572
|
-
queries: Object.entries(ParameterValue).flat(),
|
|
573
|
-
headers: Object.entries(HeaderValue).flat(),
|
|
574
|
-
cookies: Object.entries(CookieValue).flat(),
|
|
575
|
-
},
|
|
576
|
-
{
|
|
577
|
-
preferWorthWatching: false,
|
|
578
|
-
}
|
|
579
|
-
).resultsList || [];
|
|
580
567
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
568
|
+
const { ParameterValue, HeaderValue, CookieValue } = findingsForScoreRequest;
|
|
569
|
+
|
|
570
|
+
const results =
|
|
571
|
+
agentLib.scoreRequestConnect(
|
|
572
|
+
rulesMask,
|
|
573
|
+
{
|
|
574
|
+
queries: Object.entries(ParameterValue).flat(),
|
|
575
|
+
headers: Object.entries(HeaderValue).flat(),
|
|
576
|
+
cookies: Object.entries(CookieValue).flat(),
|
|
577
|
+
},
|
|
578
|
+
{
|
|
585
579
|
preferWorthWatching: false,
|
|
586
|
-
}
|
|
587
|
-
).
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
});
|
|
601
|
-
const key = ArrayPrototypeJoin.call([
|
|
602
|
-
probe.ruleId,
|
|
603
|
-
probe.inputType,
|
|
604
|
-
...probe.path,
|
|
605
|
-
probe.value,
|
|
606
|
-
], '|');
|
|
607
|
-
probes[key] = probe;
|
|
580
|
+
}
|
|
581
|
+
).resultsList || [];
|
|
582
|
+
|
|
583
|
+
Object.entries(findingsForScoreAtom).forEach(([value, inputTypes]) => {
|
|
584
|
+
Object.entries(inputTypes).forEach(([inputType, resultByRuleId]) =>
|
|
585
|
+
(
|
|
586
|
+
agentLib.scoreAtom(rulesMask, value, agentLib.InputType[inputType], {
|
|
587
|
+
preferWorthWatching: false,
|
|
588
|
+
}) || []
|
|
589
|
+
).forEach(result => {
|
|
590
|
+
results.push({ value, ...result });
|
|
591
|
+
valueToResultByRuleId[value] = resultByRuleId;
|
|
592
|
+
})
|
|
593
|
+
);
|
|
608
594
|
});
|
|
609
595
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
596
|
+
results
|
|
597
|
+
.filter(({ score, ruleId }) => score >= 90 && isMonitorMode(ruleId, sourceContext))
|
|
598
|
+
.forEach((result) => {
|
|
599
|
+
const resultByRuleId = valueToResultByRuleId[result.value];
|
|
600
|
+
const probe = Object.assign({}, resultByRuleId, result, {
|
|
601
|
+
mappedId: result.ruleId,
|
|
602
|
+
});
|
|
603
|
+
const key = ArrayPrototypeJoin.call([
|
|
604
|
+
probe.ruleId,
|
|
605
|
+
probe.inputType,
|
|
606
|
+
...probe.path,
|
|
607
|
+
probe.value,
|
|
608
|
+
], '|');
|
|
609
|
+
probes[key] = probe;
|
|
610
|
+
});
|
|
614
611
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
/**
|
|
620
|
-
* commonObjectAnalyzer() walks an object supplied by the end-user and checks
|
|
621
|
-
* it for vulnerabilities.
|
|
622
|
-
*
|
|
623
|
-
* This can cause the request to be blocked, depending on the mode and findings.
|
|
624
|
-
*
|
|
625
|
-
* @param {Object} sourceContext the sourceContext for the request
|
|
626
|
-
* @param {Object} object the object to analyze. It could be from any input
|
|
627
|
-
* source that can resolve to an object: x-www-form-urlencoded, JSON,
|
|
628
|
-
* query params.
|
|
629
|
-
* @param {Object} inputTypes is either jsonInputTypes or parameterInputTypes,
|
|
630
|
-
* both are defined above. They specify the input types to be used when evaluating
|
|
631
|
-
* the object.
|
|
632
|
-
* @returns {Array | undefined} returns an array with block info if vulnerability was found.
|
|
633
|
-
*/
|
|
634
|
-
function commonObjectAnalyzer(sourceContext, object, inputTypes) {
|
|
635
|
-
const { policy: { rulesMask } } = sourceContext;
|
|
636
|
-
if (!rulesMask) return;
|
|
637
|
-
|
|
638
|
-
// use inputTypes to set params...
|
|
639
|
-
const { keyType, inputType } = inputTypes;
|
|
640
|
-
const inputTypeStr = inputTypes === jsonInputTypes ? 'Json' : 'Parameter';
|
|
641
|
-
const resultsList = [];
|
|
642
|
-
|
|
643
|
-
// it's possible to optimize this if qs (or a similar package) is not loaded
|
|
644
|
-
// or if none of the values of queryParams are objects. a quick '.includes()'
|
|
645
|
-
// could be used to determine that. if none are objects then traverseKeysAndValues()
|
|
646
|
-
// wouldn't be used, just a simple "for (const key in queryParams) {...}" to
|
|
647
|
-
// check each key and value associated with the key.
|
|
648
|
-
//
|
|
649
|
-
// otoh, it's a fair amount of additional logic and the gain is likely to be
|
|
650
|
-
// small, so it probably only makes sense to check if qs (or similar) is actually
|
|
651
|
-
// in use. a benchmark of "for (const key in queryParams) {...}" vs traverseKeysAndValues
|
|
652
|
-
// should be created to see if, and in what cases, it makes sense.
|
|
653
|
-
//
|
|
654
|
-
// another day.
|
|
655
|
-
|
|
656
|
-
/* eslint-disable-next-line complexity */
|
|
657
|
-
traverseKeysAndValues(object, function (path, type, value) {
|
|
658
|
-
let itemType;
|
|
659
|
-
let isMongoQueryType;
|
|
660
|
-
// this is a bit awkward now because nosql-injection-mongo is not integrated
|
|
661
|
-
// into the scoreAtom() function (or the check_input() function it uses). as
|
|
662
|
-
// a result, the two rules need to be checked independently and the results
|
|
663
|
-
// merged, which is kind of a pia.
|
|
664
|
-
// TODO AGENT-205
|
|
665
|
-
if (type === 'Key') {
|
|
666
|
-
itemType = keyType;
|
|
667
|
-
if (rulesMask & agentLib.RuleType['nosql-injection-mongo']) {
|
|
668
|
-
isMongoQueryType = agentLib.isMongoQueryType(value);
|
|
612
|
+
Object.values(probes).forEach(probe => {
|
|
613
|
+
if (!resultsMap[probe.ruleId]) {
|
|
614
|
+
resultsMap[probe.ruleId] = [];
|
|
669
615
|
}
|
|
670
|
-
} else {
|
|
671
|
-
itemType = inputType;
|
|
672
|
-
}
|
|
673
616
|
|
|
674
|
-
|
|
617
|
+
resultsMap[probe.ruleId].push(probe);
|
|
618
|
+
});
|
|
619
|
+
};
|
|
675
620
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
621
|
+
/**
|
|
622
|
+
* commonObjectAnalyzer() walks an object supplied by the end-user and checks
|
|
623
|
+
* it for vulnerabilities.
|
|
624
|
+
*
|
|
625
|
+
* This can cause the request to be blocked, depending on the mode and findings.
|
|
626
|
+
*
|
|
627
|
+
* @param {Object} sourceContext the sourceContext for the request
|
|
628
|
+
* @param {Object} object the object to analyze. It could be from any input
|
|
629
|
+
* source that can resolve to an object: x-www-form-urlencoded, JSON,
|
|
630
|
+
* query params.
|
|
631
|
+
* @param {Object} inputTypes is either jsonInputTypes or parameterInputTypes,
|
|
632
|
+
* both are defined above. They specify the input types to be used when evaluating
|
|
633
|
+
* the object.
|
|
634
|
+
* @returns {Array | undefined} returns an array with block info if vulnerability was found.
|
|
635
|
+
*/
|
|
636
|
+
function commonObjectAnalyzer(sourceContext, object, inputTypes) {
|
|
637
|
+
const { policy: { rulesMask } } = sourceContext;
|
|
638
|
+
if (!rulesMask) return;
|
|
639
|
+
|
|
640
|
+
// use inputTypes to set params...
|
|
641
|
+
const { keyType, inputType } = inputTypes;
|
|
642
|
+
const inputTypeStr = inputTypes === jsonInputTypes ? 'Json' : 'Parameter';
|
|
643
|
+
const resultsList = [];
|
|
644
|
+
|
|
645
|
+
// it's possible to optimize this if qs (or a similar package) is not loaded
|
|
646
|
+
// or if none of the values of queryParams are objects. a quick '.includes()'
|
|
647
|
+
// could be used to determine that. if none are objects then traverseKeysAndValues()
|
|
648
|
+
// wouldn't be used, just a simple "for (const key in queryParams) {...}" to
|
|
649
|
+
// check each key and value associated with the key.
|
|
650
|
+
//
|
|
651
|
+
// otoh, it's a fair amount of additional logic and the gain is likely to be
|
|
652
|
+
// small, so it probably only makes sense to check if qs (or similar) is actually
|
|
653
|
+
// in use. a benchmark of "for (const key in queryParams) {...}" vs traverseKeysAndValues
|
|
654
|
+
// should be created to see if, and in what cases, it makes sense.
|
|
655
|
+
//
|
|
656
|
+
// another day.
|
|
657
|
+
|
|
658
|
+
/* eslint-disable-next-line complexity */
|
|
659
|
+
traverseKeysAndValues(object, function(path, type, value) {
|
|
660
|
+
let itemType;
|
|
661
|
+
let isMongoQueryType;
|
|
662
|
+
// this is a bit awkward now because nosql-injection-mongo is not integrated
|
|
663
|
+
// into the scoreAtom() function (or the check_input() function it uses). as
|
|
664
|
+
// a result, the two rules need to be checked independently and the results
|
|
665
|
+
// merged, which is kind of a pia.
|
|
666
|
+
// TODO AGENT-205
|
|
667
|
+
if (type === 'Key') {
|
|
668
|
+
itemType = keyType;
|
|
669
|
+
if (rulesMask & agentLib.RuleType['nosql-injection-mongo']) {
|
|
670
|
+
isMongoQueryType = agentLib.isMongoQueryType(value);
|
|
671
|
+
}
|
|
672
|
+
} else {
|
|
673
|
+
itemType = inputType;
|
|
706
674
|
}
|
|
707
|
-
resultsList.push(result);
|
|
708
|
-
}
|
|
709
|
-
});
|
|
710
|
-
|
|
711
|
-
// if nothing was found then nothing needs to be done.
|
|
712
|
-
if (resultsList.length === 0) {
|
|
713
|
-
return;
|
|
714
|
-
}
|
|
715
675
|
|
|
716
|
-
|
|
717
|
-
const findings = {
|
|
718
|
-
trackRequest: true,
|
|
719
|
-
securityException: undefined,
|
|
720
|
-
resultsList,
|
|
721
|
-
};
|
|
722
|
-
|
|
723
|
-
return mergeFindings(sourceContext, findings);
|
|
724
|
-
}
|
|
676
|
+
let items = agentLib.scoreAtom(rulesMask, value, itemType, preferWW);
|
|
725
677
|
|
|
726
|
-
|
|
727
|
-
|
|
678
|
+
if (!items && !isMongoQueryType) {
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
if (!items) {
|
|
682
|
+
items = [];
|
|
683
|
+
}
|
|
684
|
+
let mongoPath;
|
|
685
|
+
// if the key was a mongo query key, then add it to the items. it requires
|
|
686
|
+
// that additional information is kept as well.
|
|
687
|
+
if (isMongoQueryType) {
|
|
688
|
+
const inputToCheck = getValueAtKey(object, path, value);
|
|
689
|
+
// because scoreRequestConnect() returns the query type in the value, we
|
|
690
|
+
// mimic it here (where scoreAtom() was used). the actual object/string
|
|
691
|
+
// to match is stored as `inputToCheck`.
|
|
692
|
+
const item = { ruleId: 'nosql-injection-mongo', score: 10, mongoContext: { inputToCheck } };
|
|
693
|
+
items.push(item);
|
|
694
|
+
}
|
|
695
|
+
// make each item a complete Finding
|
|
696
|
+
for (const item of items) {
|
|
697
|
+
const result = {
|
|
698
|
+
ruleId: item.ruleId,
|
|
699
|
+
inputType: `${inputTypeStr}${type}`,
|
|
700
|
+
path: mongoPath || ArrayPrototypeSlice.call(path),
|
|
701
|
+
key: type === 'Key' ? value : path[path.length - 1],
|
|
702
|
+
value,
|
|
703
|
+
score: item.score,
|
|
704
|
+
idsList: item.idsList || [],
|
|
705
|
+
};
|
|
706
|
+
if (item.mongoContext) {
|
|
707
|
+
result.mongoContext = item.mongoContext;
|
|
708
|
+
}
|
|
709
|
+
resultsList.push(result);
|
|
710
|
+
}
|
|
711
|
+
});
|
|
728
712
|
|
|
729
|
-
|
|
730
|
-
if (
|
|
731
|
-
|
|
732
|
-
forwardedIps.push(...ipsFromHeaders);
|
|
713
|
+
// if nothing was found then nothing needs to be done.
|
|
714
|
+
if (resultsList.length === 0) {
|
|
715
|
+
return;
|
|
733
716
|
}
|
|
734
|
-
}
|
|
735
717
|
|
|
736
|
-
|
|
737
|
-
|
|
718
|
+
// something was found, so create "complete" findings.
|
|
719
|
+
const findings = {
|
|
720
|
+
trackRequest: true,
|
|
721
|
+
securityException: undefined,
|
|
722
|
+
resultsList,
|
|
723
|
+
};
|
|
738
724
|
|
|
739
|
-
|
|
740
|
-
if (!ipsToCheck.length) {
|
|
741
|
-
return false;
|
|
725
|
+
return mergeFindings(sourceContext, findings);
|
|
742
726
|
}
|
|
743
727
|
|
|
744
|
-
|
|
745
|
-
const
|
|
746
|
-
for (let i = 0; i < ipsToCheck.length; i++) {
|
|
747
|
-
const currentIp = ipsToCheck[i];
|
|
728
|
+
function ipListAnalysis(reqIp, reqHeaders, list) {
|
|
729
|
+
const forwardedIps = [];
|
|
748
730
|
|
|
749
|
-
|
|
750
|
-
if (
|
|
751
|
-
|
|
752
|
-
|
|
731
|
+
for (let i = 0; i < reqHeaders.length; i++) {
|
|
732
|
+
if (reqHeaders[i] === 'x-forwarded-for') {
|
|
733
|
+
const ipsFromHeaders = StringPrototypeSplit.call(reqHeaders[i + 1], /[,;]+/);
|
|
734
|
+
forwardedIps.push(...ipsFromHeaders);
|
|
753
735
|
}
|
|
754
|
-
|
|
736
|
+
}
|
|
755
737
|
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
738
|
+
const ipsToCheck = [reqIp, ...forwardedIps];
|
|
739
|
+
const now = new Date().getTime();
|
|
740
|
+
|
|
741
|
+
/* c8 ignore next 3 */
|
|
742
|
+
if (!ipsToCheck.length) {
|
|
743
|
+
return false;
|
|
744
|
+
}
|
|
760
745
|
|
|
761
|
-
|
|
746
|
+
for (const listEntry of list) {
|
|
747
|
+
const { doesExpire, expiresAt } = listEntry;
|
|
748
|
+
for (let i = 0; i < ipsToCheck.length; i++) {
|
|
749
|
+
const currentIp = ipsToCheck[i];
|
|
762
750
|
|
|
763
|
-
|
|
764
|
-
|
|
751
|
+
// Ignore bad IP values.
|
|
752
|
+
if (!address.isValid(currentIp)) {
|
|
753
|
+
logger.warn('Unable to parse %s.', currentIp);
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
const expired = doesExpire ? expiresAt - now <= 0 : false;
|
|
757
|
+
|
|
758
|
+
if (expired) {
|
|
759
|
+
logger.info('IP expired: %s, %s', listEntry.name, listEntry.ip);
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
const match = checkIpsMatch(listEntry, currentIp);
|
|
764
|
+
|
|
765
|
+
if (match) {
|
|
766
|
+
return match;
|
|
767
|
+
}
|
|
765
768
|
}
|
|
766
769
|
}
|
|
767
770
|
}
|
|
768
|
-
}
|
|
769
|
-
};
|
|
771
|
+
},
|
|
772
|
+
});
|
|
770
773
|
|
|
771
774
|
/**
|
|
772
775
|
* Reads the source context's policy and compares to result item to check whether to ignore it.
|