@contrast/agent 4.12.1 → 4.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bootstrap.js +2 -3
- package/esm.mjs +9 -35
- package/lib/assess/membrane/debraner.js +0 -2
- package/lib/assess/membrane/index.js +1 -3
- package/lib/assess/models/base-event.js +1 -1
- package/lib/assess/models/tag-range/util.js +1 -2
- package/lib/assess/policy/util.js +3 -2
- package/lib/assess/propagators/JSON/stringify.js +6 -11
- package/lib/assess/propagators/ajv/conditionals.js +0 -3
- package/lib/assess/propagators/ajv/json-schema-type-evaluators.js +5 -4
- package/lib/assess/propagators/ajv/refs.js +1 -2
- package/lib/assess/propagators/ajv/schema-context.js +2 -3
- package/lib/assess/propagators/path/common.js +38 -29
- package/lib/assess/propagators/path/resolve.js +1 -0
- package/lib/assess/propagators/sequelize/utils.js +1 -2
- package/lib/assess/propagators/v8/init-hooks.js +0 -1
- package/lib/assess/sinks/dynamo.js +65 -30
- package/lib/assess/static/hardcoded.js +3 -3
- package/lib/assess/static/read-findings-from-cache.js +40 -0
- package/lib/assess/technologies/index.js +12 -13
- package/lib/cli-rewriter/index.js +65 -6
- package/lib/core/config/options.js +6 -0
- package/lib/core/config/util.js +15 -33
- package/lib/core/exclusions/input.js +6 -1
- package/lib/core/express/index.js +2 -4
- package/lib/core/logger/debug-logger.js +2 -2
- package/lib/core/stacktrace.js +2 -1
- package/lib/hooks/http.js +81 -81
- package/lib/hooks/require.js +1 -0
- package/lib/instrumentation.js +17 -0
- package/lib/protect/analysis/aho-corasick.js +1 -1
- package/lib/protect/errors/handler-async-errors.js +66 -0
- package/lib/protect/input-analysis.js +7 -13
- package/lib/protect/listeners.js +27 -23
- package/lib/protect/rules/base-scanner/index.js +2 -2
- package/lib/protect/rules/bot-blocker/bot-blocker-rule.js +4 -2
- package/lib/protect/rules/cmd-injection/cmdinjection-rule.js +57 -2
- package/lib/protect/rules/cmd-injection-semantic-chained-commands/cmd-injection-semantic-chained-commands-rule.js +31 -2
- package/lib/protect/rules/cmd-injection-semantic-dangerous-paths/cmd-injection-semantic-dangerous-paths-rule.js +32 -2
- package/lib/protect/rules/index.js +42 -21
- package/lib/protect/rules/ip-denylist/ip-denylist-rule.js +2 -2
- package/lib/protect/rules/nosqli/nosql-injection-rule.js +104 -39
- package/lib/protect/rules/path-traversal/path-traversal-rule.js +3 -0
- package/lib/protect/rules/rule-factory.js +6 -7
- package/lib/protect/rules/signatures/signature.js +3 -0
- package/lib/protect/rules/sqli/sql-injection-rule.js +98 -5
- package/lib/protect/rules/sqli/sql-scanner/labels.json +0 -3
- package/lib/protect/rules/xss/reflected-xss-rule.js +3 -3
- package/lib/protect/sample-aggregator.js +65 -57
- package/lib/protect/service.js +709 -104
- package/lib/reporter/models/app-activity/sample.js +6 -0
- package/lib/reporter/speedracer/unknown-connection-state.js +20 -32
- package/lib/reporter/translations/to-protobuf/settings/assess-features.js +4 -6
- package/lib/reporter/ts-reporter.js +1 -1
- package/lib/util/get-file-type.js +43 -0
- package/package.json +10 -11
- package/perf-logs.js +2 -5
package/lib/hooks/http.js
CHANGED
|
@@ -75,93 +75,93 @@ const hookServer = (server, agent, id) => {
|
|
|
75
75
|
const self = this;
|
|
76
76
|
const [event, req, res] = args;
|
|
77
77
|
|
|
78
|
-
if (event
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
78
|
+
if (event !== 'request') {
|
|
79
|
+
return emit.apply(self, args);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
logger.debug('Received request %s, method %s', req.url, req.method);
|
|
83
|
+
return scopes.runInRequestScope({
|
|
84
|
+
agent,
|
|
85
|
+
req,
|
|
86
|
+
res,
|
|
87
|
+
hookResponse(response, agent) {
|
|
88
|
+
const { writeHead, end } = response;
|
|
89
|
+
// XXX(ehden): we keep the original method available to call when handling
|
|
90
|
+
// blocked requests because of MethodTampering or other rules whose sink
|
|
91
|
+
// type is RESPONSE_STATUS and which can block at perimeter. We call the original
|
|
92
|
+
// when we block to prevent recursing through blocks by setting our own 403.
|
|
93
|
+
response[WRITE_HEAD] = writeHead;
|
|
94
|
+
response[END] = end;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Patch writeHead to emit a sink event before calling writeHead
|
|
98
|
+
*
|
|
99
|
+
*/
|
|
100
|
+
patcher.patch(response, 'writeHead', {
|
|
101
|
+
alwaysRun: true,
|
|
102
|
+
name: 'write-head',
|
|
103
|
+
patchType: PATCH_TYPES.PROTECT_SINK,
|
|
104
|
+
pre(data) {
|
|
105
|
+
const [statusCode, reason] = data.args;
|
|
106
|
+
let [, , obj] = data.args;
|
|
107
|
+
let contentType;
|
|
108
|
+
if (typeof reason !== 'string') {
|
|
109
|
+
obj = reason;
|
|
110
|
+
}
|
|
111
|
+
if (obj && typeof obj === 'object') {
|
|
112
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
113
|
+
if (key.toLowerCase() === 'content-type') {
|
|
114
|
+
contentType = value;
|
|
113
115
|
}
|
|
114
116
|
}
|
|
115
|
-
if (contentType) {
|
|
116
|
-
AsyncStorage.set(KEYS.RESPONSE_CONTENT_TYPE, contentType);
|
|
117
|
-
}
|
|
118
|
-
emitSinkEvent(statusCode, SINK_TYPES.RESPONSE_STATUS, id, false);
|
|
119
117
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
118
|
+
if (contentType) {
|
|
119
|
+
AsyncStorage.set(KEYS.RESPONSE_CONTENT_TYPE, contentType);
|
|
120
|
+
}
|
|
121
|
+
emitSinkEvent(statusCode, SINK_TYPES.RESPONSE_STATUS, id, false);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
patcher.patch(response, 'setHeader', {
|
|
126
|
+
alwaysRun: true,
|
|
127
|
+
name: 'set-header',
|
|
128
|
+
patchType: PATCH_TYPES.ASYNC_CONTEXT,
|
|
129
|
+
pre(data) {
|
|
130
|
+
const [name = '', value] = data.args;
|
|
131
|
+
if (name.toLowerCase() === 'content-type' && value) {
|
|
132
|
+
AsyncStorage.set(KEYS.RESPONSE_CONTENT_TYPE, value);
|
|
131
133
|
}
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
// special HTTP logging for responses.
|
|
135
|
-
if (agent.config.agent.logger.log_outbound_http) {
|
|
136
|
-
['end', 'writeHead', 'write'].forEach((method) => {
|
|
137
|
-
logHook(response, method);
|
|
138
|
-
});
|
|
139
134
|
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
INPUT_TYPES.IP,
|
|
148
|
-
id
|
|
149
|
-
);
|
|
150
|
-
agentEmitter.emit('http.requestStart', req, res, ipEvent);
|
|
151
|
-
|
|
152
|
-
emitSourceEvent(req, req, res, 'method', INPUT_TYPES.METHOD, id);
|
|
153
|
-
emitSourceEvent(req, req, res, 'headers', INPUT_TYPES.HEADER, id);
|
|
154
|
-
emitSourceEvent(req, req, res, 'url', INPUT_TYPES.URL, id);
|
|
155
|
-
|
|
156
|
-
const rv = emit.apply(self, args);
|
|
157
|
-
|
|
158
|
-
agentEmitter.emit('http.requestEnd', req, res);
|
|
159
|
-
return rv;
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// special HTTP logging for responses.
|
|
138
|
+
if (agent.config.agent.logger.log_outbound_http) {
|
|
139
|
+
['end', 'writeHead', 'write'].forEach((method) => {
|
|
140
|
+
logHook(response, method);
|
|
141
|
+
});
|
|
160
142
|
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
143
|
+
},
|
|
144
|
+
callback() {
|
|
145
|
+
const ipEvent = createSourceEvent(
|
|
146
|
+
req,
|
|
147
|
+
req,
|
|
148
|
+
res,
|
|
149
|
+
'socket.remoteAddress',
|
|
150
|
+
INPUT_TYPES.IP,
|
|
151
|
+
id
|
|
152
|
+
);
|
|
153
|
+
agentEmitter.emit('http.requestStart', req, res, ipEvent);
|
|
154
|
+
|
|
155
|
+
emitSourceEvent(req, req, res, 'method', INPUT_TYPES.METHOD, id);
|
|
156
|
+
emitSourceEvent(req, req, res, 'headers', INPUT_TYPES.HEADER, id);
|
|
157
|
+
emitSourceEvent(req, req, res, 'url', INPUT_TYPES.URL, id);
|
|
158
|
+
|
|
159
|
+
const rv = emit.apply(self, args);
|
|
160
|
+
|
|
161
|
+
agentEmitter.emit('http.requestEnd', req, res);
|
|
162
|
+
return rv;
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
165
|
};
|
|
166
166
|
};
|
|
167
167
|
|
package/lib/hooks/require.js
CHANGED
|
@@ -22,6 +22,7 @@ const logger = require('../core/logger')('hooks:require');
|
|
|
22
22
|
class ModuleHook {
|
|
23
23
|
constructor() {
|
|
24
24
|
this.reqHook = new RequireHook(logger);
|
|
25
|
+
this.reqHook.install();
|
|
25
26
|
|
|
26
27
|
// RequireHook takes care of hooking but we still need to patch require to
|
|
27
28
|
// emit 'require' events
|
package/lib/instrumentation.js
CHANGED
|
@@ -102,6 +102,7 @@ function assessModeFeatures(agent) {
|
|
|
102
102
|
if (agent.isInAssessMode()) {
|
|
103
103
|
logger.debug('initializing assess mode features');
|
|
104
104
|
require('./assess/policy/init').init(agent);
|
|
105
|
+
require('./assess/static/read-findings-from-cache')(agent);
|
|
105
106
|
require('./hooks/encoding')();
|
|
106
107
|
require('./hooks/object-to-primitive')();
|
|
107
108
|
require('./hooks/array')();
|
|
@@ -121,6 +122,22 @@ function protectModeFeatures({ agent, reporter }) {
|
|
|
121
122
|
logger.debug('initializing protect mode features');
|
|
122
123
|
|
|
123
124
|
require('./protect')();
|
|
125
|
+
|
|
126
|
+
// if it's native_input_analysis then use agent-lib
|
|
127
|
+
if (agent.config.agent.node.native_input_analysis) {
|
|
128
|
+
const lib = require('@contrast/agent-lib');
|
|
129
|
+
// needs the || '.' for testing...
|
|
130
|
+
const logDir = agent.config.agent.node.analysis_log_dir || '.';
|
|
131
|
+
const agentLib = new lib.Agent(
|
|
132
|
+
{ enableLogging: true, logDir, logLevel: "INFO" }
|
|
133
|
+
);
|
|
134
|
+
// attach the constants so lib.Agent() isn't exposed.
|
|
135
|
+
for (const c in lib.constants) {
|
|
136
|
+
agentLib[c] = lib.constants[c];
|
|
137
|
+
}
|
|
138
|
+
agent.agentLib = agentLib;
|
|
139
|
+
}
|
|
140
|
+
|
|
124
141
|
const protectService = require('./protect/listeners')(agent, reporter);
|
|
125
142
|
const InputAnalysisSensor = require('./protect/input-analysis');
|
|
126
143
|
require('./protect/sources')(agent);
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Copyright: 2022 Contrast Security, Inc
|
|
3
|
+
Contact: support@contrastsecurity.com
|
|
4
|
+
License: Commercial
|
|
5
|
+
|
|
6
|
+
NOTICE: This Software and the patented inventions embodied within may only be
|
|
7
|
+
used as part of Contrast Security’s commercial offerings. Even though it is
|
|
8
|
+
made available through public repositories, use of this Software is subject to
|
|
9
|
+
the applicable End User Licensing Agreement found at
|
|
10
|
+
https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
|
|
11
|
+
between Contrast Security and the End User. The Software may not be reverse
|
|
12
|
+
engineered, modified, repackaged, sold, redistributed or otherwise used in a
|
|
13
|
+
way not consistent with the End User License Agreement.
|
|
14
|
+
*/
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const { KEYS, AsyncStorage } = require('../../core/async-storage');
|
|
18
|
+
const agentEmitter = require('../../agent-emitter');
|
|
19
|
+
const isContrastError = require('../../util/is-contrast-error');
|
|
20
|
+
const process = require('process');
|
|
21
|
+
|
|
22
|
+
const handledErrors = new WeakSet();
|
|
23
|
+
|
|
24
|
+
module.exports.install = function() {
|
|
25
|
+
const originalOn = process.on;
|
|
26
|
+
process.on = function (name, ...args) {
|
|
27
|
+
if (name === 'unhandledRejection') {
|
|
28
|
+
return originalOn.call(
|
|
29
|
+
this,
|
|
30
|
+
name,
|
|
31
|
+
...args.map(
|
|
32
|
+
(origHandler) =>
|
|
33
|
+
function (reason, promise) {
|
|
34
|
+
if (handledErrors.has(reason)) {
|
|
35
|
+
return; // skip
|
|
36
|
+
}
|
|
37
|
+
return origHandler(reason, promise);
|
|
38
|
+
},
|
|
39
|
+
),
|
|
40
|
+
);
|
|
41
|
+
} else {
|
|
42
|
+
return originalOn.call(this, name, ...args);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
process.on('unhandledRejection', function asyncErrorHandling(error) {
|
|
47
|
+
handlerAsyncErrors(error);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
function handlerAsyncErrors(error) {
|
|
51
|
+
let handled = null;
|
|
52
|
+
if (isContrastError(error)) {
|
|
53
|
+
const res = AsyncStorage.fromException(error, KEYS.RES);
|
|
54
|
+
handled = agentEmitter.handleError(error, res);
|
|
55
|
+
}
|
|
56
|
+
if (handled) {
|
|
57
|
+
handledErrors.add(error);
|
|
58
|
+
} else {
|
|
59
|
+
console.warn(
|
|
60
|
+
`An Unhandled Rejection has been caught by the Contrast Security node-agent instrumentation. Error: ${error}`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
|
|
@@ -35,10 +35,7 @@ class InputAnalysisSensor {
|
|
|
35
35
|
* for instrumenting both `http` and `https` modules when they load.
|
|
36
36
|
*/
|
|
37
37
|
install() {
|
|
38
|
-
if (
|
|
39
|
-
this.installed ||
|
|
40
|
-
!this.agent.config.agent.node.speedracer_input_analysis
|
|
41
|
-
) {
|
|
38
|
+
if (this.installed) {
|
|
42
39
|
return;
|
|
43
40
|
}
|
|
44
41
|
this.installed = true;
|
|
@@ -140,7 +137,7 @@ class InputAnalysisSensor {
|
|
|
140
137
|
|
|
141
138
|
let permit = true;
|
|
142
139
|
try {
|
|
143
|
-
const appContext = InputAnalysisSensor.
|
|
140
|
+
const appContext = InputAnalysisSensor.makeApplicationContext(method);
|
|
144
141
|
permit = await this.service.analyzeRequest({
|
|
145
142
|
meta,
|
|
146
143
|
req,
|
|
@@ -249,10 +246,6 @@ class InputAnalysisSensor {
|
|
|
249
246
|
return sizeInMb >= this.maxSize;
|
|
250
247
|
}
|
|
251
248
|
|
|
252
|
-
static isDone(args) {
|
|
253
|
-
return args[0] === 'end';
|
|
254
|
-
}
|
|
255
|
-
|
|
256
249
|
/**
|
|
257
250
|
* We defer calling the original req.emit of data chunks
|
|
258
251
|
* and end until SR analyzes the request body.
|
|
@@ -266,7 +259,8 @@ class InputAnalysisSensor {
|
|
|
266
259
|
*/
|
|
267
260
|
async processBodyAndEmit(meta, context, req, res) {
|
|
268
261
|
const { args, method } = context;
|
|
269
|
-
|
|
262
|
+
// the request is done when the end event is emitted
|
|
263
|
+
const done = args[0] === 'end';
|
|
270
264
|
|
|
271
265
|
let permit = true;
|
|
272
266
|
|
|
@@ -274,7 +268,7 @@ class InputAnalysisSensor {
|
|
|
274
268
|
let appContext;
|
|
275
269
|
|
|
276
270
|
if (done) {
|
|
277
|
-
appContext = InputAnalysisSensor.
|
|
271
|
+
appContext = InputAnalysisSensor.makeApplicationContext(method);
|
|
278
272
|
|
|
279
273
|
permit = await this.service.analyzeRequestStream({
|
|
280
274
|
meta,
|
|
@@ -352,10 +346,10 @@ class InputAnalysisSensor {
|
|
|
352
346
|
}
|
|
353
347
|
|
|
354
348
|
/**
|
|
355
|
-
*
|
|
349
|
+
* Makes a new app context and sets the stack with
|
|
356
350
|
* the proper request handler
|
|
357
351
|
*/
|
|
358
|
-
static
|
|
352
|
+
static makeApplicationContext(handle) {
|
|
359
353
|
const appContext = new ApplicationContext();
|
|
360
354
|
appContext.setStack(handle);
|
|
361
355
|
return appContext;
|
package/lib/protect/listeners.js
CHANGED
|
@@ -21,6 +21,7 @@ Copyright: 2022 Contrast Security, Inc
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
const _ = require('lodash');
|
|
24
|
+
|
|
24
25
|
const Stream = require('stream');
|
|
25
26
|
const parseurl = require('parseurl');
|
|
26
27
|
const agentEmitter = require('../agent-emitter');
|
|
@@ -28,25 +29,28 @@ const ApplicationContext = require('./models/application-context');
|
|
|
28
29
|
const Request = require('../reporter/models/request');
|
|
29
30
|
const {
|
|
30
31
|
AsyncStorage,
|
|
31
|
-
KEYS: { SAMPLES, RULES, INPUT_EXCLUSIONS, RES, REQ, REQUEST }
|
|
32
|
+
KEYS: { SAMPLES, RULES, INPUT_EXCLUSIONS, RES, REQ, REQUEST },
|
|
32
33
|
} = require('../core/async-storage');
|
|
33
34
|
const Samples = require('./samples.js');
|
|
34
35
|
const ProtectService = require('./service');
|
|
35
36
|
const { INPUT_TYPES } = require('../constants');
|
|
37
|
+
const handlerAsyncErrors = require('./errors/handler-async-errors');
|
|
36
38
|
|
|
37
39
|
module.exports = function protectEventListener(agent, reporter) {
|
|
38
40
|
const service = new ProtectService(agent, reporter);
|
|
39
41
|
|
|
42
|
+
handlerAsyncErrors.install();
|
|
43
|
+
|
|
40
44
|
agentEmitter.on('http.requestStart', (req, res, ipEvent) => {
|
|
41
|
-
//
|
|
42
|
-
//
|
|
45
|
+
// apply exclusions based only on the url path name, not on the full
|
|
46
|
+
// path (which may include a query string).
|
|
43
47
|
const { pathname: urlPath } = parseurl(req);
|
|
44
48
|
|
|
45
49
|
const rules = service.getEnabledRules(urlPath, ipEvent);
|
|
46
50
|
AsyncStorage.set(RULES, rules);
|
|
47
51
|
AsyncStorage.set(
|
|
48
52
|
INPUT_EXCLUSIONS,
|
|
49
|
-
service.getEnabledInputExclusions(urlPath)
|
|
53
|
+
service.getEnabledInputExclusions(urlPath),
|
|
50
54
|
);
|
|
51
55
|
AsyncStorage.set(SAMPLES, new Samples());
|
|
52
56
|
});
|
|
@@ -65,16 +69,20 @@ module.exports = function protectEventListener(agent, reporter) {
|
|
|
65
69
|
}
|
|
66
70
|
|
|
67
71
|
const ctxt = AsyncStorage.getContext();
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
enrichEvent(event, ctxt);
|
|
73
|
-
if (agent.config.agent.node.speedracer_input_analysis) {
|
|
72
|
+
if (ctxt.defend) {
|
|
73
|
+
const { exclusions: inputExclusions, samples } = ctxt.defend;
|
|
74
|
+
const rules = sourceRuleFilter(ctxt.defend.rules);
|
|
75
|
+
enrichEvent(event, ctxt);
|
|
74
76
|
enrichSamples(event, samples);
|
|
75
|
-
}
|
|
76
77
|
|
|
77
|
-
|
|
78
|
+
if (!rules || rules.length === 0) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
// it's ugly because this probably should have been here all along as opposed
|
|
82
|
+
// to "enriching" the event with data from ctxt.
|
|
83
|
+
event._ctxt = ctxt;
|
|
84
|
+
service.handleSourceEvent(event, rules, inputExclusions, samples);
|
|
85
|
+
}
|
|
78
86
|
});
|
|
79
87
|
|
|
80
88
|
agentEmitter.on('protect.sink', (event) => {
|
|
@@ -100,13 +108,13 @@ module.exports = function protectEventListener(agent, reporter) {
|
|
|
100
108
|
* @param {Object} event the SourceEvent instance
|
|
101
109
|
* @param {Set} storage async protect storage
|
|
102
110
|
*/
|
|
103
|
-
function enrichEvent(event, storageCtxt
|
|
111
|
+
function enrichEvent(event, storageCtxt) {
|
|
104
112
|
if (storageCtxt) {
|
|
105
113
|
[
|
|
106
114
|
{ key: '_incomingMessage', ctxtKey: REQ },
|
|
107
115
|
{ key: '_serverResponse', ctxtKey: RES },
|
|
108
116
|
{ key: 'request', ctxtKey: REQUEST },
|
|
109
|
-
{ key: 'response', ctxtKey: RES }
|
|
117
|
+
{ key: 'response', ctxtKey: RES },
|
|
110
118
|
].forEach(({ key, ctxtKey }) => {
|
|
111
119
|
if (!event[key]) {
|
|
112
120
|
setKey({ storageCtxt, ctxtKey, event, key });
|
|
@@ -130,11 +138,8 @@ function enrichEvent(event, storageCtxt = AsyncStorage.getContext()) {
|
|
|
130
138
|
* @param {SourceEvent} event
|
|
131
139
|
* @param {StorageContext} ctxt
|
|
132
140
|
*/
|
|
133
|
-
function enrichSamples(event, samples) {
|
|
134
|
-
if (event.type !== INPUT_TYPES.URL_PARAMETER) {
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
141
|
|
|
142
|
+
function enrichSamples(event, samples) {
|
|
138
143
|
for (const key of Object.keys(event.data)) {
|
|
139
144
|
for (const sample of samples.getAllByType(INPUT_TYPES.URL_PARAMETER)) {
|
|
140
145
|
const decoded = decodeURIComponent(sample.input.name);
|
|
@@ -189,14 +194,13 @@ function isStream(data) {
|
|
|
189
194
|
}
|
|
190
195
|
|
|
191
196
|
/**
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
* analysis, we only want a subset of active rules to respond to these events.
|
|
197
|
+
* return a function that filters out rules that speedracer or agent-lib
|
|
198
|
+
* handles.
|
|
195
199
|
* @param {Object} config The agent configuration
|
|
196
|
-
* @returns {
|
|
200
|
+
* @returns {Function} function that returns either filtered or unfiltered rules
|
|
197
201
|
*/
|
|
198
202
|
function getSourceRuleFilter(config) {
|
|
199
|
-
return config.agent.node.speedracer_input_analysis
|
|
203
|
+
return config.agent.node.speedracer_input_analysis && !config.agent.node.native_input_analysis
|
|
200
204
|
? (rules) => rules.filter((rule) => rule.inputClassification === false)
|
|
201
205
|
: (rules) => rules;
|
|
202
206
|
}
|
|
@@ -67,8 +67,8 @@ class BaseScanner {
|
|
|
67
67
|
* 1. Parsing the query into a token sequence.
|
|
68
68
|
* 2. Finding all stop/start indices of the input string within the query.
|
|
69
69
|
* 3. Comparing these to the indices of the tokens in the sequence.
|
|
70
|
-
* @param {String} substring
|
|
71
|
-
* @param {String}
|
|
70
|
+
* @param {String} substring An input string.
|
|
71
|
+
* @param {String} query The query to analyze.
|
|
72
72
|
* @returns {Object[]}
|
|
73
73
|
*/
|
|
74
74
|
findInjection(substring, query) {
|
|
@@ -23,13 +23,15 @@ const { INPUT_TYPES, IMPORTANCE, PROTECTION_MODES } = require('../common');
|
|
|
23
23
|
const USER_AGENT = 'user-agent';
|
|
24
24
|
|
|
25
25
|
class BotBlockerRule extends Rule {
|
|
26
|
-
constructor(policy = {}) {
|
|
27
|
-
super(policy);
|
|
26
|
+
constructor(policy = {}, agent) {
|
|
27
|
+
super(policy, agent);
|
|
28
28
|
this.id = 'bot-blocker';
|
|
29
29
|
this.name = 'Bot Blocker';
|
|
30
30
|
this.blockAtEntry = true;
|
|
31
31
|
this.mode = PROTECTION_MODES.BLOCK_AT_PERIMETER;
|
|
32
32
|
this.applicableInputs = [INPUT_TYPES.HEADER];
|
|
33
|
+
|
|
34
|
+
this.usesLibInputAnalysis = true;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
/**
|
|
@@ -20,10 +20,11 @@ Copyright: 2022 Contrast Security, Inc
|
|
|
20
20
|
|
|
21
21
|
const Rule = require('../');
|
|
22
22
|
const { INPUT_TYPES, SINK_TYPES } = require('../common');
|
|
23
|
+
const logger = require('../../../core/logger')('contrast:rules:protect');
|
|
23
24
|
|
|
24
25
|
class CMDInjectionRule extends Rule {
|
|
25
|
-
constructor(policy) {
|
|
26
|
-
super(policy);
|
|
26
|
+
constructor(policy, agent) {
|
|
27
|
+
super(policy, agent);
|
|
27
28
|
|
|
28
29
|
this.id = 'cmd-injection';
|
|
29
30
|
this.name = 'Command Injection';
|
|
@@ -42,6 +43,45 @@ class CMDInjectionRule extends Rule {
|
|
|
42
43
|
INPUT_TYPES.URL_PARAMETER
|
|
43
44
|
];
|
|
44
45
|
this.applicableSinks = [SINK_TYPES.COMMAND];
|
|
46
|
+
|
|
47
|
+
this.usesLibInputAnalysis = true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Calls down to the agent analysis library for evaluation with the
|
|
52
|
+
* @param {Samples} applicableSamples Samples cache
|
|
53
|
+
* @param {Set} params.applicableSamples samples applicable to rule id
|
|
54
|
+
*/
|
|
55
|
+
evaluateAtSinkForLib({ event, applicableSamples }) {
|
|
56
|
+
if (applicableSamples.size == 0 || !event.data) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for (const sample of applicableSamples) {
|
|
61
|
+
let evalResult = null;
|
|
62
|
+
try {
|
|
63
|
+
const input = sample.input.value;
|
|
64
|
+
const sinkData = event.data;
|
|
65
|
+
const inputIndex = sinkData.indexOf(input);
|
|
66
|
+
|
|
67
|
+
if (inputIndex !== -1) {
|
|
68
|
+
evalResult = this.agent.agentLib.checkCommandInjectionSink(
|
|
69
|
+
inputIndex,
|
|
70
|
+
input.length,
|
|
71
|
+
input
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
} catch (e) {
|
|
75
|
+
logger.info(`Failed to evaluate command-injection sink: ${e}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (evalResult) {
|
|
79
|
+
this.appendAttackDetails(sample, evalResult);
|
|
80
|
+
sample.captureAppContext(event);
|
|
81
|
+
logger.warn(`EFFECTIVE - rule: ${this.id}, mode: ${this.mode}`);
|
|
82
|
+
this.blockRequest(sample);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
45
85
|
}
|
|
46
86
|
|
|
47
87
|
/**
|
|
@@ -53,6 +93,21 @@ class CMDInjectionRule extends Rule {
|
|
|
53
93
|
buildDetails(sample, findings) {
|
|
54
94
|
return { command: findings };
|
|
55
95
|
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Builds the details for TS UI rendering based on agent lib findings.
|
|
99
|
+
* @param {Sample} sample relevant protect sample
|
|
100
|
+
* @param {Object} findings The results from the agent library
|
|
101
|
+
* @returns {Object}
|
|
102
|
+
*/
|
|
103
|
+
buildDetailsForLib(sample, findings) {
|
|
104
|
+
return {
|
|
105
|
+
command: sample.input.value.substring(
|
|
106
|
+
findings.startIndex,
|
|
107
|
+
findings.endIndex
|
|
108
|
+
)
|
|
109
|
+
};
|
|
110
|
+
}
|
|
56
111
|
}
|
|
57
112
|
|
|
58
113
|
module.exports = CMDInjectionRule;
|
|
@@ -24,8 +24,8 @@ const UserInputFactory = require('../../../reporter/models/utils/user-input-fact
|
|
|
24
24
|
const ChainedCommandScanner = require('./chained-command-scanner');
|
|
25
25
|
|
|
26
26
|
class CMDInjectionSemanticChainedCommandsRule extends Rule {
|
|
27
|
-
constructor(policy) {
|
|
28
|
-
super(policy);
|
|
27
|
+
constructor(policy, agent) {
|
|
28
|
+
super(policy, agent);
|
|
29
29
|
this.id = 'cmd-injection-semantic-chained-commands';
|
|
30
30
|
this.name = 'Command Injection Chained Commands';
|
|
31
31
|
this.applicableSinks = [SINK_TYPES.COMMAND];
|
|
@@ -72,6 +72,35 @@ class CMDInjectionSemanticChainedCommandsRule extends Rule {
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Performs semantic analysis to determine if there are chained subcommands
|
|
77
|
+
* using agent-lib
|
|
78
|
+
* @param {ApplicationContext} event Sink event
|
|
79
|
+
* @param {Request} request Request model
|
|
80
|
+
* @param {Samples} samples Samples cache
|
|
81
|
+
*/
|
|
82
|
+
evaluateAtSinkForLib({ event, request, samples }) {
|
|
83
|
+
const { data: command } = event;
|
|
84
|
+
const index = this.agent.agentLib.indexOfChaining(command);
|
|
85
|
+
if (index != -1) {
|
|
86
|
+
const sample = this.createAndSaveSample({
|
|
87
|
+
input: UserInputFactory.makeOne({ value: command }),
|
|
88
|
+
attributes: { effective: true },
|
|
89
|
+
classification: IMPORTANCE.DEFINITE,
|
|
90
|
+
event,
|
|
91
|
+
samples,
|
|
92
|
+
request
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
this.appendAttackDetails(sample, {
|
|
96
|
+
command,
|
|
97
|
+
findings: [CMD_INJECTION_SEMANTIC_TYPES.CHAINING]
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
this.blockRequest(sample);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
75
104
|
/**
|
|
76
105
|
* Builds the details for TS UI rendering.
|
|
77
106
|
* @param {Sample} sample N/a in this rule's case
|