@contrast/agent 4.12.2 → 4.14.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/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/tag-range/util.js +1 -2
- package/lib/assess/policy/propagators.json +13 -4
- package/lib/assess/policy/rules.json +42 -0
- package/lib/assess/policy/signatures.json +18 -0
- 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/joi/any.js +1 -1
- package/lib/assess/propagators/joi/object.js +1 -1
- package/lib/assess/propagators/joi/string-base.js +16 -3
- package/lib/assess/propagators/mongoose/map.js +1 -1
- package/lib/assess/propagators/mongoose/mixed.js +1 -1
- package/lib/assess/propagators/mongoose/string.js +1 -1
- 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/async-storage/hooks/mysql.js +57 -6
- package/lib/core/config/options.js +12 -6
- 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 +11 -11
- package/perf-logs.js +2 -5
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
|
|
@@ -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) {
|