@contrast/agent 4.12.0 → 4.13.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/bin/VERSION +1 -1
- package/bin/linux/contrast-service +0 -0
- package/bin/mac/contrast-service +0 -0
- package/bin/windows/contrast-service.exe +0 -0
- package/esm.mjs +1 -32
- package/lib/assess/models/base-event.js +1 -1
- package/lib/assess/sinks/dynamo.js +65 -30
- 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/contrast.js +1 -2
- 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/hooks/http.js +81 -81
- package/lib/hooks/require.js +1 -0
- package/lib/instrumentation.js +17 -0
- 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 +103 -38
- package/lib/protect/rules/path-traversal/path-traversal-rule.js +3 -0
- package/lib/protect/rules/rule-factory.js +6 -6
- 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/ts-reporter.js +1 -1
- package/lib/util/get-file-type.js +47 -0
- package/package.json +5 -3
|
@@ -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) => {
|
|
@@ -15,6 +15,7 @@ Copyright: 2022 Contrast Security, Inc
|
|
|
15
15
|
'use strict';
|
|
16
16
|
/* eslint-disable complexity */
|
|
17
17
|
const _ = require('lodash');
|
|
18
|
+
const util = require('util');
|
|
18
19
|
|
|
19
20
|
const logger = require('../../../core/logger')('contrast:rules:protect');
|
|
20
21
|
const { INPUT_TYPES, SINK_TYPES } = require('../common');
|
|
@@ -32,12 +33,12 @@ const ScannerKit = new Map([
|
|
|
32
33
|
[MONGODB, () => require('../nosqli/nosql-scanner').create('MongoDB')],
|
|
33
34
|
[RETHINKDB, () => require('../nosqli/nosql-scanner').create('RethinkDB')]
|
|
34
35
|
]);
|
|
35
|
-
const SubstringFinder = require('../base-scanner/substring-finder');
|
|
36
36
|
|
|
37
37
|
class NoSqlInjectionRule extends require('../') {
|
|
38
|
-
|
|
38
|
+
// eslint-disable-next-line default-param-last
|
|
39
|
+
constructor(policy = {}, agent) {
|
|
39
40
|
policy.inputParseDepth = 3;
|
|
40
|
-
super(policy);
|
|
41
|
+
super(policy, agent);
|
|
41
42
|
|
|
42
43
|
this._scanners = new Map();
|
|
43
44
|
|
|
@@ -52,56 +53,64 @@ class NoSqlInjectionRule extends require('../') {
|
|
|
52
53
|
INPUT_TYPES.QUERYSTRING,
|
|
53
54
|
INPUT_TYPES.XML_VALUE,
|
|
54
55
|
INPUT_TYPES.URI,
|
|
55
|
-
INPUT_TYPES.URL_PARAMETER
|
|
56
|
+
INPUT_TYPES.URL_PARAMETER,
|
|
57
|
+
INPUT_TYPES.BODY,
|
|
58
|
+
INPUT_TYPES.MULTIPART_VALUE,
|
|
56
59
|
];
|
|
60
|
+
|
|
61
|
+
// POST bodies are handled by the lib.
|
|
62
|
+
// if lib is disabled, node input analysis needs to handle this.
|
|
63
|
+
if (!this.agentLibEnabled) {
|
|
64
|
+
this.applicableInputs.push(INPUT_TYPES.BODY);
|
|
65
|
+
}
|
|
66
|
+
|
|
57
67
|
this.applicableSinks = [SINK_TYPES.NOSQL_QUERY];
|
|
68
|
+
|
|
69
|
+
// explicitly set to false so that this rule will continue to use node input analysis.
|
|
70
|
+
this.usesLibInputAnalysis = false;
|
|
58
71
|
}
|
|
59
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Evaluates the sink data by scanning the document object for repeated instances
|
|
75
|
+
* of the saved input object/strings. This applies to both string injection and
|
|
76
|
+
* object expansion.
|
|
77
|
+
* @param {SinkEvent} event Emitted by database drivers/wrappers/ORMs
|
|
78
|
+
* @param {Set} applicableSamples set of Samples applicable to rule.
|
|
79
|
+
*/
|
|
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'),
|
|
@@ -50,11 +49,12 @@ const ctors = {
|
|
|
50
49
|
};
|
|
51
50
|
|
|
52
51
|
class ProtectRuleFactory {
|
|
53
|
-
constructor({ featureSet = DEFAULT_SETTINGS, enabled }) {
|
|
52
|
+
constructor({ featureSet = DEFAULT_SETTINGS, enabled, agent }) {
|
|
54
53
|
this.featureSet = featureSet;
|
|
55
54
|
this.settings = {
|
|
56
55
|
exceptions: []
|
|
57
56
|
};
|
|
57
|
+
this.agent = agent;
|
|
58
58
|
this.policies = {};
|
|
59
59
|
this.signatures = new SignatureKit();
|
|
60
60
|
|
|
@@ -180,7 +180,7 @@ class ProtectRuleFactory {
|
|
|
180
180
|
*/
|
|
181
181
|
IpDenylist() {
|
|
182
182
|
const policy = this.policies[RULES.IP_DENYLIST];
|
|
183
|
-
return new ctors[RULES.IP_DENYLIST](policy);
|
|
183
|
+
return new ctors[RULES.IP_DENYLIST](policy, this.agent);
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
/**
|
|
@@ -189,7 +189,7 @@ class ProtectRuleFactory {
|
|
|
189
189
|
*/
|
|
190
190
|
BotBlocker() {
|
|
191
191
|
const policy = this.policies[RULES.BOT_BLOCKER];
|
|
192
|
-
return new ctors[RULES.BOT_BLOCKER](policy);
|
|
192
|
+
return new ctors[RULES.BOT_BLOCKER](policy, this.agent);
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
/**
|
|
@@ -198,7 +198,7 @@ class ProtectRuleFactory {
|
|
|
198
198
|
*/
|
|
199
199
|
VirtualPatchRules() {
|
|
200
200
|
return this.policies[RULES.VIRTUAL_PATCH].map(
|
|
201
|
-
(policy) => new ctors[RULES.VIRTUAL_PATCH](policy)
|
|
201
|
+
(policy) => new ctors[RULES.VIRTUAL_PATCH](policy, this.agent)
|
|
202
202
|
);
|
|
203
203
|
}
|
|
204
204
|
|
|
@@ -224,7 +224,7 @@ class ProtectRuleFactory {
|
|
|
224
224
|
policy.enable_rep = enableRep;
|
|
225
225
|
|
|
226
226
|
if (ProtectRuleFactory.shouldBuildRule(policy)) {
|
|
227
|
-
const rule = new ctors[id](policy);
|
|
227
|
+
const rule = new ctors[id](policy, this.agent);
|
|
228
228
|
rule.signature = this.signatures.get(id);
|
|
229
229
|
memo.push(rule);
|
|
230
230
|
}
|
|
@@ -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
|
|