@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
|
@@ -24,8 +24,8 @@ const UserInputFactory = require('../../../reporter/models/utils/user-input-fact
|
|
|
24
24
|
const DangerousPathsScanner = require('./dangerous-paths-scanner');
|
|
25
25
|
|
|
26
26
|
class CMDInjectionSemanticDangerousPathsRule extends Rule {
|
|
27
|
-
constructor(policy) {
|
|
28
|
-
super(policy);
|
|
27
|
+
constructor(policy, agent) {
|
|
28
|
+
super(policy, agent);
|
|
29
29
|
this.id = 'cmd-injection-semantic-dangerous-paths';
|
|
30
30
|
this.name = 'Command Injection Dangerous Paths';
|
|
31
31
|
this.applicableSinks = [SINK_TYPES.COMMAND];
|
|
@@ -66,6 +66,36 @@ class CMDInjectionSemanticDangerousPathsRule extends Rule {
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Check for dangerous paths in the input command using the agent library.
|
|
71
|
+
* @param {ApplicationContext} event Sink event
|
|
72
|
+
* @param {Request} request Request model
|
|
73
|
+
* @param {Samples} samples Samples cache
|
|
74
|
+
*/
|
|
75
|
+
evaluateAtSinkForLib({ event, request, samples }) {
|
|
76
|
+
const { data: command } = event;
|
|
77
|
+
const containsDangerousPath = this.agent.agentLib.containsDangerousPath(
|
|
78
|
+
command
|
|
79
|
+
);
|
|
80
|
+
if (containsDangerousPath) {
|
|
81
|
+
const sample = this.createAndSaveSample({
|
|
82
|
+
input: UserInputFactory.makeOne({ value: command }),
|
|
83
|
+
attributes: { effective: true },
|
|
84
|
+
classification: IMPORTANCE.DEFINITE,
|
|
85
|
+
event,
|
|
86
|
+
samples,
|
|
87
|
+
request
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
this.appendAttackDetails(sample, {
|
|
91
|
+
command,
|
|
92
|
+
findings: [CMD_INJECTION_SEMANTIC_TYPES.PATH_ARGUMENT]
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
this.blockRequest(sample);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
69
99
|
/**
|
|
70
100
|
* Builds the details for TS UI rendering.
|
|
71
101
|
* @param {Sample} sample N/a in this rule's case
|
|
@@ -28,13 +28,37 @@ const {
|
|
|
28
28
|
} = require('./common');
|
|
29
29
|
|
|
30
30
|
class Rule {
|
|
31
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Rules that need to make use of the agent lib can also take
|
|
33
|
+
* a second argument: `agent' which is a reference to the agent object.
|
|
34
|
+
*/
|
|
35
|
+
constructor(policy, agent) {
|
|
36
|
+
if (!policy) {
|
|
37
|
+
policy = { mode: NO_ACTION };
|
|
38
|
+
}
|
|
32
39
|
this.policy = policy;
|
|
33
40
|
|
|
34
41
|
this.blockAtEntry = policy.mode === BLOCK_AT_PERIMETER;
|
|
35
42
|
this.id = policy.id;
|
|
36
43
|
this.mode = policy.mode;
|
|
37
44
|
this.name = policy.name;
|
|
45
|
+
this.applicableInputs = [];
|
|
46
|
+
this.applicableSinks = [];
|
|
47
|
+
this.agent = agent;
|
|
48
|
+
const config = (this.agent && this.agent.config) || { agent: { node: {} } };
|
|
49
|
+
|
|
50
|
+
this.agentLibEnabled =
|
|
51
|
+
config.agent.node.native_input_analysis &&
|
|
52
|
+
config.agent.node.speedracer_input_analysis;
|
|
53
|
+
|
|
54
|
+
if (this.agentLibEnabled) {
|
|
55
|
+
this.evaluator = null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// override this value if the rule uses agent library input analysis.
|
|
59
|
+
// all rules that agentLibBit applies to use this field with the single
|
|
60
|
+
// exception of nosqli which uses node input analysis.
|
|
61
|
+
this.usesLibInputAnalysis = false;
|
|
38
62
|
|
|
39
63
|
// Samples --> {}
|
|
40
64
|
// This lets us manage non-sample state for each rule. This
|
|
@@ -66,10 +90,7 @@ class Rule {
|
|
|
66
90
|
* @returns {Boolean}
|
|
67
91
|
*/
|
|
68
92
|
appliesToInputType(type, name) {
|
|
69
|
-
return (
|
|
70
|
-
_.isEmpty(this.applicableInputs) ||
|
|
71
|
-
_.includes(this.applicableInputs, type)
|
|
72
|
-
);
|
|
93
|
+
return this.applicableInputs.length === 0 || this.applicableInputs.includes(type);
|
|
73
94
|
}
|
|
74
95
|
|
|
75
96
|
/**
|
|
@@ -79,11 +100,7 @@ class Rule {
|
|
|
79
100
|
* @returns {Boolean}
|
|
80
101
|
*/
|
|
81
102
|
appliesToSink(type) {
|
|
82
|
-
|
|
83
|
-
return false;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return _.includes(this.applicableSinks, type);
|
|
103
|
+
return this.applicableSinks.includes(type);
|
|
87
104
|
}
|
|
88
105
|
|
|
89
106
|
/**
|
|
@@ -105,12 +122,6 @@ class Rule {
|
|
|
105
122
|
return this.signature;
|
|
106
123
|
}
|
|
107
124
|
|
|
108
|
-
if (!this.evaluator) {
|
|
109
|
-
throw new Error(
|
|
110
|
-
`Rule ${this.id} has no custom evaluator for signature-matching/classification`
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
125
|
return this.evaluator;
|
|
115
126
|
}
|
|
116
127
|
|
|
@@ -144,6 +155,7 @@ class Rule {
|
|
|
144
155
|
const name = sample.input ? sample.input.name : '<anon>';
|
|
145
156
|
const type = sample.input ? sample.input.type : '<no-type>';
|
|
146
157
|
|
|
158
|
+
// TODO: should this be ||?
|
|
147
159
|
if (!this.blockAtEntry && !sample.effective) {
|
|
148
160
|
return;
|
|
149
161
|
}
|
|
@@ -172,6 +184,10 @@ class Rule {
|
|
|
172
184
|
preFilterUserInput(input, event, samples) {
|
|
173
185
|
const evaluator = this.getEvaluator();
|
|
174
186
|
|
|
187
|
+
if (!evaluator) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
175
191
|
// This is to support the newer API accepting a UserInput,
|
|
176
192
|
let evaluation;
|
|
177
193
|
if (this.signature) {
|
|
@@ -188,7 +204,7 @@ class Rule {
|
|
|
188
204
|
samples
|
|
189
205
|
});
|
|
190
206
|
|
|
191
|
-
if (
|
|
207
|
+
if (sample && sample.confirmedAttack === true && this.mode === BLOCK_AT_PERIMETER) {
|
|
192
208
|
this.blockRequest(sample);
|
|
193
209
|
}
|
|
194
210
|
}
|
|
@@ -247,7 +263,7 @@ class Rule {
|
|
|
247
263
|
|
|
248
264
|
logger.debug(`${classifiedAs}: ${id} - ${type}.${name}.${value}`);
|
|
249
265
|
if (classifiedAs === IMPORTANCE.NONE) {
|
|
250
|
-
return;
|
|
266
|
+
return null;
|
|
251
267
|
}
|
|
252
268
|
|
|
253
269
|
const sample = samples.addRuleSample({
|
|
@@ -277,8 +293,8 @@ class Rule {
|
|
|
277
293
|
// for CEF extensions
|
|
278
294
|
outcome: this.getAttackStatus(sample),
|
|
279
295
|
pri: this.id,
|
|
280
|
-
request:
|
|
281
|
-
spt:
|
|
296
|
+
request: (sample.request.uri || '').replace(/ /g, '<space>'),
|
|
297
|
+
spt: sample.request.port || '',
|
|
282
298
|
requestMethod: sample.request.method,
|
|
283
299
|
src: sample.request.ip,
|
|
284
300
|
value: sample.input.value
|
|
@@ -329,7 +345,12 @@ class Rule {
|
|
|
329
345
|
*/
|
|
330
346
|
appendAttackDetails(sample, findings) {
|
|
331
347
|
sample.effective = true;
|
|
332
|
-
|
|
348
|
+
|
|
349
|
+
if (!(this.usesLibInputAnalysis && this.agentLibEnabled) || !this.buildDetailsForLib) {
|
|
350
|
+
sample.details = this.buildDetails(sample, findings);
|
|
351
|
+
} else {
|
|
352
|
+
sample.details = this.buildDetailsForLib(sample, findings);
|
|
353
|
+
}
|
|
333
354
|
}
|
|
334
355
|
}
|
|
335
356
|
|
|
@@ -26,8 +26,8 @@ const {
|
|
|
26
26
|
const Rule = require('../');
|
|
27
27
|
|
|
28
28
|
class IpDenylistRule extends Rule {
|
|
29
|
-
constructor(policy) {
|
|
30
|
-
super(policy);
|
|
29
|
+
constructor(policy, agent) {
|
|
30
|
+
super(policy, agent);
|
|
31
31
|
this._analyzer = new IpAnalyzer(policy.data);
|
|
32
32
|
// TODO: This should go away with CONTRAST-34184
|
|
33
33
|
this._analyzer.on('expired', (expired) => {
|
|
@@ -13,8 +13,8 @@ Copyright: 2022 Contrast Security, Inc
|
|
|
13
13
|
way not consistent with the End User License Agreement.
|
|
14
14
|
*/
|
|
15
15
|
'use strict';
|
|
16
|
-
/* eslint-disable complexity */
|
|
17
16
|
const _ = require('lodash');
|
|
17
|
+
const util = require('util');
|
|
18
18
|
|
|
19
19
|
const logger = require('../../../core/logger')('contrast:rules:protect');
|
|
20
20
|
const { INPUT_TYPES, SINK_TYPES } = require('../common');
|
|
@@ -32,12 +32,12 @@ const ScannerKit = new Map([
|
|
|
32
32
|
[MONGODB, () => require('../nosqli/nosql-scanner').create('MongoDB')],
|
|
33
33
|
[RETHINKDB, () => require('../nosqli/nosql-scanner').create('RethinkDB')]
|
|
34
34
|
]);
|
|
35
|
-
const SubstringFinder = require('../base-scanner/substring-finder');
|
|
36
35
|
|
|
37
36
|
class NoSqlInjectionRule extends require('../') {
|
|
38
|
-
|
|
37
|
+
// eslint-disable-next-line default-param-last
|
|
38
|
+
constructor(policy = {}, agent) {
|
|
39
39
|
policy.inputParseDepth = 3;
|
|
40
|
-
super(policy);
|
|
40
|
+
super(policy, agent);
|
|
41
41
|
|
|
42
42
|
this._scanners = new Map();
|
|
43
43
|
|
|
@@ -52,56 +52,65 @@ class NoSqlInjectionRule extends require('../') {
|
|
|
52
52
|
INPUT_TYPES.QUERYSTRING,
|
|
53
53
|
INPUT_TYPES.XML_VALUE,
|
|
54
54
|
INPUT_TYPES.URI,
|
|
55
|
-
INPUT_TYPES.URL_PARAMETER
|
|
55
|
+
INPUT_TYPES.URL_PARAMETER,
|
|
56
|
+
INPUT_TYPES.BODY,
|
|
57
|
+
INPUT_TYPES.MULTIPART_VALUE,
|
|
56
58
|
];
|
|
59
|
+
|
|
60
|
+
// POST bodies are handled by the lib.
|
|
61
|
+
// if lib is disabled, node input analysis needs to handle this.
|
|
62
|
+
if (!this.agentLibEnabled) {
|
|
63
|
+
this.applicableInputs.push(INPUT_TYPES.BODY);
|
|
64
|
+
}
|
|
65
|
+
|
|
57
66
|
this.applicableSinks = [SINK_TYPES.NOSQL_QUERY];
|
|
67
|
+
|
|
68
|
+
// explicitly set to false so that this rule will continue to use node input analysis.
|
|
69
|
+
this.usesLibInputAnalysis = false;
|
|
58
70
|
}
|
|
59
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Evaluates the sink data by scanning the document object for repeated instances
|
|
74
|
+
* of the saved input object/strings. This applies to both string injection and
|
|
75
|
+
* object expansion.
|
|
76
|
+
* @param {SinkEvent} event Emitted by database drivers/wrappers/ORMs
|
|
77
|
+
* @param {Set} applicableSamples set of Samples applicable to rule.
|
|
78
|
+
*/
|
|
79
|
+
// eslint-disable-next-line complexity
|
|
60
80
|
evaluateAtSink({ event, applicableSamples }) {
|
|
61
|
-
if (
|
|
81
|
+
if (applicableSamples.size == 0 || !event.data) {
|
|
62
82
|
return;
|
|
63
83
|
}
|
|
64
84
|
|
|
65
|
-
|
|
85
|
+
const { data } = event;
|
|
86
|
+
if (typeof data === 'object') {
|
|
66
87
|
for (const sample of applicableSamples) {
|
|
67
|
-
const
|
|
68
|
-
if (!
|
|
88
|
+
const doc = this.getInput(sample);
|
|
89
|
+
if (!doc) {
|
|
69
90
|
return;
|
|
70
91
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
92
|
+
|
|
93
|
+
let evalResult = null;
|
|
94
|
+
traverse(data, (key, value) => {
|
|
95
|
+
if (evalResult) {
|
|
74
96
|
return;
|
|
75
97
|
}
|
|
76
98
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
99
|
+
// check for _exact_ match under clause for saved user input
|
|
100
|
+
for (const queryClause of Object.keys(doc)) {
|
|
101
|
+
if (key === queryClause) {
|
|
102
|
+
if (_.isEqual(value, doc[queryClause])) {
|
|
103
|
+
evalResult = this.buildFinding(data, queryClause, doc);
|
|
104
|
+
}
|
|
81
105
|
}
|
|
82
|
-
}
|
|
106
|
+
}
|
|
83
107
|
});
|
|
84
108
|
|
|
85
|
-
if (
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (location) {
|
|
91
|
-
injection = {
|
|
92
|
-
input: found,
|
|
93
|
-
location,
|
|
94
|
-
query
|
|
95
|
-
};
|
|
96
|
-
break;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
if (injection) {
|
|
100
|
-
this.appendAttackDetails(sample, injection);
|
|
101
|
-
sample.captureAppContext(event);
|
|
102
|
-
logger.warn(`EFFECTIVE - rule: ${this.id}, mode: ${this.mode}`);
|
|
103
|
-
this.blockRequest(sample);
|
|
104
|
-
}
|
|
109
|
+
if (evalResult) {
|
|
110
|
+
this.appendAttackDetails(sample, evalResult);
|
|
111
|
+
sample.captureAppContext(event);
|
|
112
|
+
logger.warn(`EFFECTIVE - rule: ${this.id}, mode: ${this.mode}`);
|
|
113
|
+
this.blockRequest(sample);
|
|
105
114
|
}
|
|
106
115
|
}
|
|
107
116
|
} else if (typeof event.data === 'string' || event.data instanceof String) {
|
|
@@ -120,6 +129,47 @@ class NoSqlInjectionRule extends require('../') {
|
|
|
120
129
|
}
|
|
121
130
|
}
|
|
122
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Depending on the sample's data, collect the query clause and object
|
|
134
|
+
* for a given input sample. Handles both body inputs and other types.
|
|
135
|
+
* If the library is enabled and the input is a JSON body, it will parse
|
|
136
|
+
* query clauses out and pass them in _inputInfoForSink.
|
|
137
|
+
* @param {Sample} sample applicable sample for nosqli
|
|
138
|
+
* @returns {Object} relevant query clause and doc object to search for.
|
|
139
|
+
*/
|
|
140
|
+
getInput(sample) {
|
|
141
|
+
if (!_.isEmpty(sample._inputInfoForSink)) {
|
|
142
|
+
return {
|
|
143
|
+
[sample._inputInfoForSink.queryClause]: sample._inputInfoForSink.docObject
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const reqData = this.getRequestData(sample.input);
|
|
148
|
+
|
|
149
|
+
return reqData;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Build the query object based on the inputs passed and stringify them for reporting.
|
|
154
|
+
* @param {String} queryClause the mongo query clause (ex: $ne, $where, etc.)
|
|
155
|
+
* @param {Object} docObject the document object passed to the mongo function.
|
|
156
|
+
*/
|
|
157
|
+
buildFinding(queryObject, queryClause, docObject) {
|
|
158
|
+
// this input string being generated is kinda duplicate work from the input stage
|
|
159
|
+
// but it is only really hit when a nosqli is found, so I'm not sure its a big deal.
|
|
160
|
+
const input = util.inspect(docObject, false, null);
|
|
161
|
+
const query = util.inspect(queryObject, false, null);
|
|
162
|
+
// this substring being generated is some duplicate work.
|
|
163
|
+
const start = query.indexOf(input);
|
|
164
|
+
return {
|
|
165
|
+
input,
|
|
166
|
+
// account for query clause, ':' and ' '.
|
|
167
|
+
start: start - queryClause.length - 2,
|
|
168
|
+
end: start + (queryClause.length + 2),
|
|
169
|
+
query
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
123
173
|
/**
|
|
124
174
|
* Given the sample's user input object, will read the value from the request
|
|
125
175
|
* from the async storage context.
|
|
@@ -149,7 +199,7 @@ class NoSqlInjectionRule extends require('../') {
|
|
|
149
199
|
this.buildPathArray(_documentPath, pathArray);
|
|
150
200
|
} else {
|
|
151
201
|
// only qs params and body are "expandable"
|
|
152
|
-
pathArray.push('
|
|
202
|
+
pathArray.push('query', ..._documentPath.split('.'));
|
|
153
203
|
}
|
|
154
204
|
|
|
155
205
|
let data;
|
|
@@ -213,9 +263,24 @@ class NoSqlInjectionRule extends require('../') {
|
|
|
213
263
|
return null;
|
|
214
264
|
}
|
|
215
265
|
|
|
266
|
+
// if it's not the old node format, then it's the agent-lib format.
|
|
267
|
+
// yeah, i know it's ugly.
|
|
268
|
+
if (!findings.boundary) {
|
|
269
|
+
const { start, end, query } = findings;
|
|
270
|
+
return {
|
|
271
|
+
start,
|
|
272
|
+
end,
|
|
273
|
+
// since there are no actual `token boundary overruns' in nosqli,
|
|
274
|
+
// are these not the exact same as start and end?
|
|
275
|
+
boundaryOverrunIndex: end,
|
|
276
|
+
inputBoundaryIndex: start,
|
|
277
|
+
query
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
216
281
|
const { boundary, location, query } = findings;
|
|
217
282
|
let inputBoundaryIndex, boundaryOverrunIndex;
|
|
218
|
-
const start = location[0];
|
|
283
|
+
const start = location[0]; // eslint-disable-line
|
|
219
284
|
const end = location[1] + 1;
|
|
220
285
|
|
|
221
286
|
if (boundary) {
|
|
@@ -27,7 +27,6 @@ const DEFAULT_SETTINGS = { serverFeatures: { defend: { enabled: false } } };
|
|
|
27
27
|
* implemented, we add its constructor path to provide support.
|
|
28
28
|
*/
|
|
29
29
|
const ctors = {
|
|
30
|
-
/* eslint-disable prettier/prettier */
|
|
31
30
|
// protect rules
|
|
32
31
|
[RULES.CMD_INJECTION]: require('./cmd-injection/cmdinjection-rule'),
|
|
33
32
|
[RULES.CMD_INJECTION_COMMAND_BACKDOORS]: require('./cmd-injection-command-backdoors/cmd-injection-command-backdoors-rule'),
|
|
@@ -46,15 +45,15 @@ const ctors = {
|
|
|
46
45
|
[RULES.BOT_BLOCKER]: require('./bot-blocker/bot-blocker-rule'),
|
|
47
46
|
[RULES.IP_DENYLIST]: require('./ip-denylist/ip-denylist-rule'),
|
|
48
47
|
[RULES.VIRTUAL_PATCH]: require('./virtual-patch/virtual-patch-rule'),
|
|
49
|
-
/* eslint-enable */
|
|
50
48
|
};
|
|
51
49
|
|
|
52
50
|
class ProtectRuleFactory {
|
|
53
|
-
constructor({ featureSet = DEFAULT_SETTINGS, enabled }) {
|
|
51
|
+
constructor({ featureSet = DEFAULT_SETTINGS, enabled, agent }) {
|
|
54
52
|
this.featureSet = featureSet;
|
|
55
53
|
this.settings = {
|
|
56
54
|
exceptions: []
|
|
57
55
|
};
|
|
56
|
+
this.agent = agent;
|
|
58
57
|
this.policies = {};
|
|
59
58
|
this.signatures = new SignatureKit();
|
|
60
59
|
|
|
@@ -180,7 +179,7 @@ class ProtectRuleFactory {
|
|
|
180
179
|
*/
|
|
181
180
|
IpDenylist() {
|
|
182
181
|
const policy = this.policies[RULES.IP_DENYLIST];
|
|
183
|
-
return new ctors[RULES.IP_DENYLIST](policy);
|
|
182
|
+
return new ctors[RULES.IP_DENYLIST](policy, this.agent);
|
|
184
183
|
}
|
|
185
184
|
|
|
186
185
|
/**
|
|
@@ -189,7 +188,7 @@ class ProtectRuleFactory {
|
|
|
189
188
|
*/
|
|
190
189
|
BotBlocker() {
|
|
191
190
|
const policy = this.policies[RULES.BOT_BLOCKER];
|
|
192
|
-
return new ctors[RULES.BOT_BLOCKER](policy);
|
|
191
|
+
return new ctors[RULES.BOT_BLOCKER](policy, this.agent);
|
|
193
192
|
}
|
|
194
193
|
|
|
195
194
|
/**
|
|
@@ -198,7 +197,7 @@ class ProtectRuleFactory {
|
|
|
198
197
|
*/
|
|
199
198
|
VirtualPatchRules() {
|
|
200
199
|
return this.policies[RULES.VIRTUAL_PATCH].map(
|
|
201
|
-
(policy) => new ctors[RULES.VIRTUAL_PATCH](policy)
|
|
200
|
+
(policy) => new ctors[RULES.VIRTUAL_PATCH](policy, this.agent)
|
|
202
201
|
);
|
|
203
202
|
}
|
|
204
203
|
|
|
@@ -224,7 +223,7 @@ class ProtectRuleFactory {
|
|
|
224
223
|
policy.enable_rep = enableRep;
|
|
225
224
|
|
|
226
225
|
if (ProtectRuleFactory.shouldBuildRule(policy)) {
|
|
227
|
-
const rule = new ctors[id](policy);
|
|
226
|
+
const rule = new ctors[id](policy, this.agent);
|
|
228
227
|
rule.signature = this.signatures.get(id);
|
|
229
228
|
memo.push(rule);
|
|
230
229
|
}
|
|
@@ -162,6 +162,9 @@ const createPatterns = (patternDefs) =>
|
|
|
162
162
|
class Signature {
|
|
163
163
|
constructor(definition) {
|
|
164
164
|
this.name = definition.name;
|
|
165
|
+
// the following is undefined if the rule is not an input to the
|
|
166
|
+
// agent-lib score functions.
|
|
167
|
+
this.agentLibBit = definition.agentLibBit;
|
|
165
168
|
this.keywordSearchers = normalizeScores(
|
|
166
169
|
createKeywords(definition.keywordsList)
|
|
167
170
|
);
|
|
@@ -15,7 +15,6 @@ Copyright: 2022 Contrast Security, Inc
|
|
|
15
15
|
'use strict';
|
|
16
16
|
|
|
17
17
|
const _ = require('lodash');
|
|
18
|
-
|
|
19
18
|
const logger = require('../../../core/logger')('contrast:rules:protect');
|
|
20
19
|
const { IMPORTANCE, INPUT_TYPES, SINK_TYPES } = require('../common');
|
|
21
20
|
const isGenericComplicated = require('./generic-complicated');
|
|
@@ -38,8 +37,8 @@ const ScannerKit = new Map([
|
|
|
38
37
|
]);
|
|
39
38
|
|
|
40
39
|
class SQLInjectionRule extends require('../') {
|
|
41
|
-
constructor(policy) {
|
|
42
|
-
super(policy);
|
|
40
|
+
constructor(policy, agent) {
|
|
41
|
+
super(policy, agent);
|
|
43
42
|
|
|
44
43
|
this._scanners = new Map();
|
|
45
44
|
|
|
@@ -57,15 +56,29 @@ class SQLInjectionRule extends require('../') {
|
|
|
57
56
|
INPUT_TYPES.QUERYSTRING,
|
|
58
57
|
INPUT_TYPES.XML_VALUE,
|
|
59
58
|
INPUT_TYPES.URI,
|
|
60
|
-
INPUT_TYPES.URL_PARAMETER
|
|
59
|
+
INPUT_TYPES.URL_PARAMETER,
|
|
61
60
|
];
|
|
62
61
|
this.applicableSinks = [SINK_TYPES.SQL_QUERY];
|
|
62
|
+
|
|
63
|
+
// if not using agentLib this constructor is done.
|
|
64
|
+
if (!agent.agentLib) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
this.dbFlavors = {};
|
|
69
|
+
for (const flavor in agent.agentLib.DbType) {
|
|
70
|
+
this.dbFlavors[flavor.toLowerCase()] = this.agent.agentLib.DbType[flavor];
|
|
71
|
+
}
|
|
72
|
+
this.dbFlavorKeys = Object.keys(this.dbFlavors);
|
|
73
|
+
|
|
74
|
+
this.usesLibInputAnalysis = true;
|
|
63
75
|
}
|
|
64
76
|
|
|
65
77
|
/**
|
|
66
78
|
* Evaluates the sink data by scanning the code string for injections
|
|
67
79
|
* by collected sample inputs.
|
|
68
80
|
* @param {SinkEvent} event Emitted by database drivers/wrappers/ORMs
|
|
81
|
+
* @param {Set(Sample)} applicableSamples Samples matching sql-injection criteria
|
|
69
82
|
*/
|
|
70
83
|
evaluateAtSink({ event, applicableSamples }) {
|
|
71
84
|
if (_.isEmpty(applicableSamples) || !event.data) {
|
|
@@ -88,9 +101,65 @@ class SQLInjectionRule extends require('../') {
|
|
|
88
101
|
}
|
|
89
102
|
}
|
|
90
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Evaluates the sink data by scanning the code string for injections
|
|
106
|
+
* by collected sample inputs. Rather than using the node evaluators,
|
|
107
|
+
* this takes advantage of the agent-lib.
|
|
108
|
+
* @param {SinkEvent} event Emitted by database drivers/wrappers/ORMs
|
|
109
|
+
* @param {Sample[]} applicableSamples samples that apply to SQL injection.
|
|
110
|
+
*/
|
|
111
|
+
// eslint-disable-next-line complexity
|
|
112
|
+
evaluateAtSinkForLib({ event, applicableSamples }) {
|
|
113
|
+
if (applicableSamples.size == 0 || !event.data) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const tag = this.dbFlavorKeys.find((name) => event.tags.has(name));
|
|
118
|
+
const dialect = this.dbFlavors[tag] || this.dbFlavors.mysql;
|
|
119
|
+
|
|
120
|
+
for (const sample of applicableSamples) {
|
|
121
|
+
let evalResult = null;
|
|
122
|
+
try {
|
|
123
|
+
const input = sample.input.value;
|
|
124
|
+
const sinkData = event.data;
|
|
125
|
+
const inputIndex = sinkData.indexOf(input);
|
|
126
|
+
|
|
127
|
+
if (inputIndex !== -1) {
|
|
128
|
+
evalResult = this.agent.agentLib.checkSqlInjectionSink(
|
|
129
|
+
inputIndex,
|
|
130
|
+
input.length,
|
|
131
|
+
dialect,
|
|
132
|
+
sinkData
|
|
133
|
+
);
|
|
134
|
+
if (evalResult) {
|
|
135
|
+
// capture the query for reporting purposes.
|
|
136
|
+
evalResult.query = sinkData;
|
|
137
|
+
} else if (inputIndex === 0 && input.length === sinkData.length) {
|
|
138
|
+
evalResult = {
|
|
139
|
+
startIndex: 0,
|
|
140
|
+
endIndex: input.length - 1,
|
|
141
|
+
overrunIndex: 0,
|
|
142
|
+
boundaryIndex: 0,
|
|
143
|
+
sinkData,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
} catch (e) {
|
|
148
|
+
logger.info(`Failed to evaluate command-injection sink: ${e}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (evalResult) {
|
|
152
|
+
this.appendAttackDetails(sample, evalResult);
|
|
153
|
+
sample.captureAppContext(event);
|
|
154
|
+
logger.warn(`EFFECTIVE - rule: ${this.id}, mode: ${this.mode}`);
|
|
155
|
+
this.blockRequest(sample);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
91
160
|
/**
|
|
92
161
|
* In addition to using the traditional signature matching, this rule will
|
|
93
|
-
* also check
|
|
162
|
+
* also check whether the input has other characteristics that warrant bumping
|
|
94
163
|
* an evaluation's importance from NONE -> WORTH_WATCHING.
|
|
95
164
|
* @returns {}
|
|
96
165
|
*/
|
|
@@ -141,6 +210,30 @@ class SQLInjectionRule extends require('../') {
|
|
|
141
210
|
};
|
|
142
211
|
}
|
|
143
212
|
|
|
213
|
+
/**
|
|
214
|
+
* Builds details for Sql Injection Attack from agent-lib evaluation
|
|
215
|
+
* results.
|
|
216
|
+
* @param {Sample} sample The Sample for the attack
|
|
217
|
+
* @param {Object} findings The results of the sink analysis
|
|
218
|
+
* @returns {Object} The details
|
|
219
|
+
*/
|
|
220
|
+
buildDetailsForLib(sample, finding) {
|
|
221
|
+
if (!finding) {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const { query } = finding;
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
start: finding.startIndex,
|
|
229
|
+
end: finding.endIndex,
|
|
230
|
+
input: sample.input.toSerializable(),
|
|
231
|
+
boundaryOverrunIndex: finding.overrunIndex,
|
|
232
|
+
inputBoundaryIndex: finding.boundaryIndex,
|
|
233
|
+
query
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
144
237
|
/**
|
|
145
238
|
* Returns the appropriate scanner for the SQL dialect.
|
|
146
239
|
* @param {String} id The id of the scanner
|
|
@@ -14,8 +14,6 @@ Copyright: 2022 Contrast Security, Inc
|
|
|
14
14
|
*/
|
|
15
15
|
'use strict';
|
|
16
16
|
|
|
17
|
-
const _ = require('lodash');
|
|
18
|
-
|
|
19
17
|
const logger = require('../../../core/logger')('contrast:rules:protect');
|
|
20
18
|
const { INPUT_TYPES, SINK_TYPES } = require('../common');
|
|
21
19
|
|
|
@@ -40,6 +38,8 @@ class ReflectedXssRule extends require('../') {
|
|
|
40
38
|
INPUT_TYPES.URL_PARAMETER
|
|
41
39
|
];
|
|
42
40
|
this.applicableSinks = [SINK_TYPES.RESPONSE_BODY];
|
|
41
|
+
|
|
42
|
+
this.usesLibInputAnalysis = true;
|
|
43
43
|
}
|
|
44
44
|
/**
|
|
45
45
|
* Evaluates a SinkEvent contingent on <code>applicableSinks</code>.
|
|
@@ -48,7 +48,7 @@ class ReflectedXssRule extends require('../') {
|
|
|
48
48
|
* @param {Set<Sample>} params.applicableSamples all (definite and ww) samples for xss
|
|
49
49
|
*/
|
|
50
50
|
evaluateAtSink({ event, applicableSamples }) {
|
|
51
|
-
if (!applicableSamples.size
|
|
51
|
+
if (!applicableSamples.size) {
|
|
52
52
|
return;
|
|
53
53
|
}
|
|
54
54
|
|