contrast-agent 7.4.0 → 7.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/contrast/agent/hooks/at_exit_hook.rb +16 -1
- data/lib/contrast/agent/middleware/middleware.rb +1 -1
- data/lib/contrast/agent/protect/input_analyzer/input_analyzer.rb +19 -12
- data/lib/contrast/agent/protect/input_analyzer/worth_watching_analyzer.rb +55 -20
- data/lib/contrast/agent/protect/policy/rule_applicator.rb +1 -4
- data/lib/contrast/agent/protect/rule/base.rb +56 -25
- data/lib/contrast/agent/protect/rule/bot_blocker/bot_blocker.rb +12 -4
- data/lib/contrast/agent/protect/rule/bot_blocker/bot_blocker_input_classification.rb +0 -26
- data/lib/contrast/agent/protect/rule/cmdi/cmd_injection.rb +2 -5
- data/lib/contrast/agent/protect/rule/cmdi/cmdi_backdoors.rb +2 -4
- data/lib/contrast/agent/protect/rule/cmdi/cmdi_base_rule.rb +2 -1
- data/lib/contrast/agent/protect/rule/deserialization/deserialization.rb +4 -4
- data/lib/contrast/agent/protect/rule/input_classification/base.rb +1 -4
- data/lib/contrast/agent/protect/rule/input_classification/encoding.rb +34 -2
- data/lib/contrast/agent/protect/rule/no_sqli/no_sqli.rb +5 -2
- data/lib/contrast/agent/protect/rule/path_traversal/path_traversal.rb +12 -7
- data/lib/contrast/agent/protect/rule/path_traversal/path_traversal_semantic_security_bypass.rb +2 -2
- data/lib/contrast/agent/protect/rule/sqli/sqli_base_rule.rb +2 -3
- data/lib/contrast/agent/protect/rule/sqli/sqli_semantic/sqli_dangerous_functions.rb +3 -4
- data/lib/contrast/agent/protect/rule/unsafe_file_upload/unsafe_file_upload.rb +3 -0
- data/lib/contrast/agent/protect/rule/utils/builders.rb +3 -4
- data/lib/contrast/agent/protect/rule/utils/filters.rb +32 -16
- data/lib/contrast/agent/protect/rule/xss/xss.rb +80 -0
- data/lib/contrast/agent/protect/rule/xxe/xxe.rb +9 -2
- data/lib/contrast/agent/reporting/details/xss_match.rb +17 -0
- data/lib/contrast/agent/reporting/input_analysis/input_analysis.rb +32 -0
- data/lib/contrast/agent/reporting/input_analysis/input_type.rb +4 -34
- data/lib/contrast/agent/reporting/reporting_events/finding.rb +1 -5
- data/lib/contrast/agent/reporting/reporting_events/preflight_message.rb +2 -5
- data/lib/contrast/agent/reporting/reporting_utilities/build_preflight.rb +4 -4
- data/lib/contrast/agent/reporting/reporting_utilities/reporter_client_utils.rb +5 -1
- data/lib/contrast/agent/reporting/reporting_utilities/response_handler_utils.rb +1 -1
- data/lib/contrast/agent/request/request_context_extend.rb +0 -2
- data/lib/contrast/agent/version.rb +1 -1
- data/lib/contrast/components/assess.rb +4 -0
- data/lib/contrast/framework/rails/support.rb +2 -2
- data/lib/contrast/logger/cef_log.rb +30 -4
- data/lib/contrast/utils/io_util.rb +3 -0
- data/lib/contrast/utils/json.rb +1 -1
- data/lib/contrast/utils/log_utils.rb +21 -10
- data/ruby-agent.gemspec +3 -2
- metadata +18 -12
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f6b34ada48cf9e91e05d85ea0f003575119b751bf135ef49709fb1501c475b3e
|
4
|
+
data.tar.gz: 2915fb037c832fec213dfc7835b8d732e1c22005987ef2c4287dfb55264d41a2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 627d1959c5993100f6bc08624d1a68627b02af18f6aa48c074f0cf374925877761b09077ce21366b6d5ddafb3185907472564753b3fbab65f6f7d74d2b74dc10
|
7
|
+
data.tar.gz: 224228b9e9c276c39e6d78a330a456b37cf550b4155c4b96f5ff1d29e7bf7846521c102f510061f8d6ec5e9245b99fe567cce7a5202dd2acfd62c6e7bb9ca795
|
@@ -27,7 +27,8 @@ module Contrast
|
|
27
27
|
pp_id: @ppid,
|
28
28
|
process_pid: Process.pid,
|
29
29
|
process_pp_id: Process.ppid)
|
30
|
-
|
30
|
+
$stdout.puts('[Contrast Agent] Graceful shutdown...')
|
31
|
+
report_traces
|
31
32
|
context = Contrast::Agent::REQUEST_TRACKER.current
|
32
33
|
return unless context
|
33
34
|
|
@@ -39,6 +40,20 @@ module Contrast
|
|
39
40
|
end
|
40
41
|
Contrast::Agent.reporter&.send_event_immediately(context.activity)
|
41
42
|
end
|
43
|
+
|
44
|
+
def self.report_traces
|
45
|
+
return unless Contrast::ASSESS.enabled?
|
46
|
+
|
47
|
+
collection = Contrast::Agent::Reporting::ReportingStorage.collection
|
48
|
+
|
49
|
+
# report gathered traces:
|
50
|
+
return if collection.empty?
|
51
|
+
|
52
|
+
collection.each do |_id, finding|
|
53
|
+
preflight = Contrast::Agent::Reporting::BuildPreflight.generate(finding)
|
54
|
+
Contrast::Agent.reporter&.send_event_immediately(preflight)
|
55
|
+
end
|
56
|
+
end
|
42
57
|
end
|
43
58
|
end
|
44
59
|
end
|
@@ -191,7 +191,7 @@ module Contrast
|
|
191
191
|
# Now we can build the ia_results only for postfilter rules.
|
192
192
|
context.protect_postfilter_ia
|
193
193
|
# Process Worth Watching Inputs for v2 rules
|
194
|
-
Contrast::Agent.worth_watching_analyzer&.add_to_queue(context
|
194
|
+
Contrast::Agent.worth_watching_analyzer&.add_to_queue(context)
|
195
195
|
|
196
196
|
if Contrast::Agent.framework_manager.streaming?(env)
|
197
197
|
context.reset_activity
|
@@ -20,6 +20,7 @@ require 'contrast/agent/protect/rule/xss/reflected_xss_input_classification'
|
|
20
20
|
require 'contrast/agent/protect/rule/xss/xss'
|
21
21
|
require 'contrast/components/logger'
|
22
22
|
require 'contrast/utils/object_share'
|
23
|
+
require 'contrast/utils/duck_utils'
|
23
24
|
require 'contrast/agent/protect/rule/input_classification/base64_statistic'
|
24
25
|
require 'json'
|
25
26
|
|
@@ -33,10 +34,10 @@ module Contrast
|
|
33
34
|
DISPOSITION_FILENAME = 'filename'
|
34
35
|
PREFILTER_RULES = %w[bot-blocker unsafe-file-upload reflected-xss].cs__freeze
|
35
36
|
INFILTER_RULES = %w[
|
36
|
-
sql-injection cmd-injection
|
37
|
+
sql-injection cmd-injection bot-blocker unsafe-file-upload path-traversal
|
37
38
|
nosql-injection
|
38
39
|
].cs__freeze
|
39
|
-
POSTFILTER_RULES = %w[sql-injection cmd-injection
|
40
|
+
POSTFILTER_RULES = %w[sql-injection cmd-injection path-traversal nosql-injection].cs__freeze
|
40
41
|
AGENTLIB_TIMEOUT = 5.cs__freeze
|
41
42
|
TIMEOUT_ERROR_MESSAGE = '[AgentLib] Timed out when processing InputAnalysisResult'
|
42
43
|
STANDARD_ERROR_MESSAGE = '[InputAnalyzer] Exception raise while doing input analysis:'
|
@@ -100,12 +101,15 @@ module Contrast
|
|
100
101
|
# @param input_analysis [Contrast::Agent::Reporting::InputAnalysis] from analyze method.
|
101
102
|
# @param interval [Integer] The timeout determined for the AgentLib analysis to be performed.
|
102
103
|
def input_classification_for rule_id, input_analysis, interval: AGENTLIB_TIMEOUT
|
103
|
-
return
|
104
|
+
return if input_analysis.analysed_rules.include?(rule_id)
|
105
|
+
return if input_analysis.no_inputs?
|
104
106
|
return unless (protect_rule = Contrast::PROTECT.rule(rule_id)) && protect_rule.enabled?
|
105
107
|
|
106
108
|
input_analysis.inputs.each do |input_type, value|
|
107
|
-
|
109
|
+
value = handle_header(input_type, value)
|
110
|
+
next if Contrast::Utils::DuckUtils.empty_duck?(value)
|
108
111
|
|
112
|
+
# Traverse only the Header values:
|
109
113
|
Timeout.timeout(interval) do
|
110
114
|
protect_rule.classification.classify(rule_id, input_type, value, input_analysis)
|
111
115
|
end
|
@@ -128,14 +132,12 @@ module Contrast
|
|
128
132
|
# for each protect rule.
|
129
133
|
# @param prefilter [Boolean] flag to set input analysis for prefilter rules only
|
130
134
|
# @param postfilter [Boolean] flag to set input analysis for postfilter rules.
|
131
|
-
# @param infilter [Boolean]
|
132
135
|
# @param interval [Integer] The timeout determined for the AgentLib analysis to be performed
|
133
136
|
# @return input_analysis [Contrast::Agent::Reporting::InputAnalysis, nil]
|
134
137
|
# @raise [Timeout::Error] If timeout is met.
|
135
138
|
def input_classification(input_analysis,
|
136
139
|
prefilter: false,
|
137
140
|
postfilter: false,
|
138
|
-
infilter: false,
|
139
141
|
interval: AGENTLIB_TIMEOUT)
|
140
142
|
return unless input_analysis
|
141
143
|
|
@@ -147,17 +149,22 @@ module Contrast
|
|
147
149
|
INFILTER_RULES
|
148
150
|
end
|
149
151
|
|
150
|
-
rules.each
|
151
|
-
# Check to see if rules is already triggered only for infilter:
|
152
|
-
next if input_analysis.triggered_rules.include?(rule_id) && infilter
|
153
|
-
|
154
|
-
input_classification_for(rule_id, input_analysis, interval: interval)
|
155
|
-
end
|
152
|
+
rules.each { |rule_id| input_classification_for(rule_id, input_analysis, interval: interval) }
|
156
153
|
input_analysis
|
157
154
|
end
|
158
155
|
|
159
156
|
private
|
160
157
|
|
158
|
+
# Extracts header values, and skips keys if input_type is Header.
|
159
|
+
# @param input_type [Symbol]
|
160
|
+
# @param value [Hash, nil]
|
161
|
+
# @return value [Hash, nil]
|
162
|
+
def handle_header input_type, value
|
163
|
+
return value&.values || [] if input_type == Contrast::Agent::Reporting::InputType::HEADER
|
164
|
+
|
165
|
+
value
|
166
|
+
end
|
167
|
+
|
161
168
|
# Extract the filename and name of the Content Disposition Header.
|
162
169
|
#
|
163
170
|
# @param inputs [Hash<Contrast::Agent::Protect::InputType => user_inputs>]
|
@@ -42,16 +42,15 @@ module Contrast
|
|
42
42
|
next if queue.empty?
|
43
43
|
|
44
44
|
report = false
|
45
|
-
|
46
|
-
|
47
|
-
results = build_results(stored_ia)
|
48
|
-
activity = Contrast::Agent::Reporting::ApplicationActivity.new(ia_request: stored_ia.request)
|
45
|
+
stored_context, stored_ia, results, activity = extract_from_context
|
46
|
+
|
49
47
|
results.each do |result|
|
50
|
-
next unless (attack_result = eval_input(result))
|
48
|
+
next unless (attack_result = eval_input(stored_context, result, stored_ia))
|
51
49
|
|
52
50
|
activity.attach_defend(attack_result)
|
53
51
|
report = true
|
54
52
|
end
|
53
|
+
|
55
54
|
report_activity(activity) if report
|
56
55
|
# Handle reporting of IA Cache statistics:
|
57
56
|
enqueue_cache_event(stored_ia.request)
|
@@ -62,9 +61,22 @@ module Contrast
|
|
62
61
|
end
|
63
62
|
end
|
64
63
|
|
65
|
-
#
|
66
|
-
|
67
|
-
|
64
|
+
# build attack_results for all infilter active protect rules.
|
65
|
+
# Stored Context will update the logger context and build attack results for protect rules.
|
66
|
+
# Note: call only in thread loop as it extracts from the queue.
|
67
|
+
#
|
68
|
+
# @return [Array<stored_context, stored_ia, results, activity>]
|
69
|
+
def extract_from_context
|
70
|
+
stored_context = queue.pop
|
71
|
+
stored_ia = stored_context.agent_input_analysis
|
72
|
+
results = build_results(stored_ia)
|
73
|
+
activity = Contrast::Agent::Reporting::ApplicationActivity.new(ia_request: stored_ia.request)
|
74
|
+
[stored_context, stored_ia, results, activity]
|
75
|
+
end
|
76
|
+
|
77
|
+
# @param context [Contrast::Agent::RequestContext]
|
78
|
+
def add_to_queue context
|
79
|
+
return unless context
|
68
80
|
|
69
81
|
if queue.size >= QUEUE_SIZE
|
70
82
|
logger.debug('[WorthWatchingAnalyzer] queue at max size, skip input_result')
|
@@ -74,7 +86,7 @@ module Contrast
|
|
74
86
|
# we need to save the ia which contains the request and saved extracted user inputs to
|
75
87
|
# be evaluated on the thread rather than building results here. This way we allow the
|
76
88
|
# request to continue and will build the attack results later.
|
77
|
-
queue <<
|
89
|
+
queue << context.dup
|
78
90
|
end
|
79
91
|
|
80
92
|
private
|
@@ -91,6 +103,9 @@ module Contrast
|
|
91
103
|
Contrast::Agent::Protect::InputAnalyzer.lru_cache.clear_statistics
|
92
104
|
end
|
93
105
|
|
106
|
+
# Enqueue for Telemetry reporting all base64 related events.
|
107
|
+
#
|
108
|
+
# @param request [Contrast::Agent::Request] stored request.
|
94
109
|
def enqueue_encoding_event request
|
95
110
|
return unless Contrast::Agent::Telemetry::Base.enabled?
|
96
111
|
return unless Contrast::PROTECT.normalize_base64?
|
@@ -102,16 +117,16 @@ module Contrast
|
|
102
117
|
|
103
118
|
# This method will build the attack results from the saved ia.
|
104
119
|
#
|
105
|
-
# @param
|
120
|
+
# @param stored_ia [Contrast::Agent::Reporting::InputAnalysis]
|
106
121
|
# @return attack_results [array<Contrast::Agent::Reporting::InputAnalysisResult>] all the results
|
107
122
|
# from the input analysis.
|
108
|
-
def build_results
|
123
|
+
def build_results stored_ia
|
109
124
|
# Construct the input analysis for the all the infilter rules that were not triggered.
|
110
125
|
# There is a set timeout for each rule to be analyzed in. The infilter flag will make
|
111
126
|
# sure that if a rule is already triggered during the infilter phase it will not be analyzed
|
112
127
|
# now, making sure we don't report same rule twice.
|
113
|
-
Contrast::Agent::Protect::InputAnalyzer.input_classification(
|
114
|
-
results =
|
128
|
+
Contrast::Agent::Protect::InputAnalyzer.input_classification(stored_ia)
|
129
|
+
results = stored_ia.results.reject do |val|
|
115
130
|
val.score_level == Contrast::Agent::Reporting::InputAnalysisResult::SCORE_LEVEL::IGNORE
|
116
131
|
end
|
117
132
|
return results if results
|
@@ -119,39 +134,59 @@ module Contrast
|
|
119
134
|
[]
|
120
135
|
end
|
121
136
|
|
137
|
+
# Evaluates the stored ia results and builds attack results if any.
|
138
|
+
#
|
139
|
+
# @param stored_context [Contrast::Agent::RequestContext]
|
122
140
|
# @param ia_result Contrast::Agent::Reporting::InputAnalysisResult the WorthWatching InputAnalysisResult
|
141
|
+
# @param stored_ia [Contrast::Agent::Reporting::InputAnalysis] the stored InputAnalysis
|
123
142
|
# @return [Contrast::Agent::Reporting::AttackResult, nil] InputAnalysisResult updated Result or nil
|
124
|
-
def eval_input ia_result
|
125
|
-
return
|
143
|
+
def eval_input stored_context, ia_result, stored_ia
|
144
|
+
return skip_log if ia_result.value.to_s.bytesize >= INPUT_BYTESIZE_THRESHOLD
|
126
145
|
|
127
|
-
|
128
|
-
#{ INPUT_BYTESIZE_THRESHOLD / 1024 }KB")
|
129
|
-
nil
|
146
|
+
build_attack_result(stored_context, ia_result, stored_ia)
|
130
147
|
end
|
131
148
|
|
149
|
+
# Creates new Attack Event per rule that will be triggered or probed.
|
150
|
+
#
|
151
|
+
# @param stored_context [Contrast::Agent::RequestContext]
|
132
152
|
# @param ia_result Contrast::Agent::Reporting::InputAnalysisResult the updated InputAnalysisResult
|
133
153
|
# with a score of :DEFINITEATTACK
|
154
|
+
# @param stored_ia [Contrast::Agent::Reporting::InputAnalysis] the stored InputAnalysis
|
134
155
|
# @return [Contrast::Agent::Reporting::AttackResult] the attack result from
|
135
156
|
# this input
|
136
|
-
def build_attack_result ia_result
|
137
|
-
|
157
|
+
def build_attack_result stored_context, ia_result, stored_ia
|
158
|
+
return if stored_ia.triggered_rules.include?(ia_result.rule_id)
|
159
|
+
|
160
|
+
Contrast::PROTECT.rule(ia_result.rule_id).build_attack_without_match(stored_context, ia_result, nil)
|
138
161
|
end
|
139
162
|
|
163
|
+
# @return [Queue]
|
140
164
|
def queue
|
141
165
|
@_queue ||= Queue.new
|
142
166
|
end
|
143
167
|
|
168
|
+
# Reports all gather activities to batch.
|
169
|
+
#
|
170
|
+
# @param activity [Contrast::Agent::Reporting::ApplicationActivity]
|
144
171
|
def report_activity activity
|
145
172
|
logger.debug('[WorthWatchingAnalyzer] preparing to send activity batch')
|
146
173
|
add_activity_to_batch(activity)
|
147
174
|
report_batch
|
148
175
|
end
|
149
176
|
|
177
|
+
# Deletes Queue and closes it.
|
150
178
|
def delete_queue!
|
151
179
|
@_queue&.clear
|
152
180
|
@_queue&.close
|
153
181
|
@_queue = nil
|
154
182
|
end
|
183
|
+
|
184
|
+
# Logs a message that the input was skipped because it was too large.
|
185
|
+
def skip_log
|
186
|
+
logger.debug("[WorthWatchingAnalyzer] Skipping analysis: Input size is larger than
|
187
|
+
#{ INPUT_BYTESIZE_THRESHOLD / 1024 }KB")
|
188
|
+
nil
|
189
|
+
end
|
155
190
|
end
|
156
191
|
end
|
157
192
|
end
|
@@ -55,10 +55,7 @@ module Contrast
|
|
55
55
|
return unless (ia = context.agent_input_analysis)
|
56
56
|
|
57
57
|
Contrast::Agent::Protect::InputAnalyzer.input_classification_for(rule_id, ia)
|
58
|
-
|
59
|
-
# as already it's input applicable types has been analysed.
|
60
|
-
ia.triggered_rules << rule_name
|
61
|
-
ia
|
58
|
+
context.agent_input_analysis.record_analysed_rule(rule_id)
|
62
59
|
end
|
63
60
|
|
64
61
|
protected
|
@@ -141,20 +141,30 @@ module Contrast
|
|
141
141
|
end
|
142
142
|
|
143
143
|
# With this we log to CEF
|
144
|
-
#
|
145
|
-
# @param result [Contrast::Agent::Reporting::AttackResult]
|
144
|
+
# @param result [Contrast::Agent::Reporting::InputAnalysisResult]
|
146
145
|
# @param attack [Symbol] the type of message we want to send
|
147
146
|
# @param value [String] the input value we want to log
|
148
|
-
|
147
|
+
# @param input_type [String] the input type we want to log
|
148
|
+
# @param context [Contrast::Agent::RequestContext]
|
149
|
+
def cef_logging result, attack = :ineffective_attack, value: nil, input_type: nil, context: nil
|
149
150
|
sample = result.samples[0]
|
150
151
|
outcome = result.response.to_s
|
151
|
-
input_type = sample
|
152
|
-
input_value = sample
|
153
|
-
cef_logger.send(attack, result.rule_id, outcome, input_type, input_value)
|
152
|
+
input_type = sample&.user_input&.input_type&.to_s || input_type
|
153
|
+
input_value = sample&.user_input&.value || value
|
154
|
+
cef_logger.send(attack, result.rule_id, outcome, input_type, input_value, context)
|
154
155
|
end
|
155
156
|
|
156
157
|
protected
|
157
158
|
|
159
|
+
# Records the rule being triggered at sink.
|
160
|
+
#
|
161
|
+
# @param context [Contrast::Agent::RequestContext]
|
162
|
+
def record_triggered context
|
163
|
+
return unless context
|
164
|
+
|
165
|
+
context.agent_input_analysis.record_rule_triggered(rule_name)
|
166
|
+
end
|
167
|
+
|
158
168
|
# Assign the mode from active settings.
|
159
169
|
#
|
160
170
|
# @return mode [Symbol]
|
@@ -191,22 +201,20 @@ module Contrast
|
|
191
201
|
# @param potential_attack_string [String, nil]
|
192
202
|
# @param ia_results [Array<Contrast::Agent::Reporting::InputAnalysis>]
|
193
203
|
# @param **kwargs
|
194
|
-
# @return [Contrast::Agent::Reporting, nil]
|
204
|
+
# @return [Contrast::Agent::Reporting::AttackResult, nil]
|
195
205
|
def find_attacker_with_results context, potential_attack_string, ia_results, **kwargs
|
196
206
|
logger.trace('Checking vectors for attacks', rule: rule_name, input: potential_attack_string)
|
207
|
+
return unless ia_results&.any?
|
208
|
+
return build_attack_without_match(context, ia_results[0], nil, **kwargs) unless potential_attack_string
|
197
209
|
|
198
|
-
result = nil
|
199
210
|
ia_results.each do |ia_result|
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
else
|
206
|
-
result = build_attack_without_match(context, ia_result, result, **kwargs)
|
207
|
-
end
|
211
|
+
idx = potential_attack_string.index(ia_result.value)
|
212
|
+
next unless idx
|
213
|
+
|
214
|
+
result = build_attack_with_match(context, ia_result, result || nil, potential_attack_string, **kwargs)
|
215
|
+
return result if result
|
208
216
|
end
|
209
|
-
|
217
|
+
nil
|
210
218
|
end
|
211
219
|
|
212
220
|
# By default, rules do not have to find attackers as they do not have
|
@@ -231,15 +239,17 @@ module Contrast
|
|
231
239
|
#
|
232
240
|
# @param context [Contrast::Agent::RequestContext] the context for
|
233
241
|
# the current request
|
234
|
-
# @param ia_result [Contrast::Agent::Reporting::
|
242
|
+
# @param ia_result [Contrast::Agent::Reporting::Settings::InputAnalysisResult]
|
235
243
|
# @param result [Contrast::Agent::Reporting::AttackResult]
|
236
244
|
# @param attack_string [String] Potential attack vector
|
237
245
|
# @return [Contrast::Agent::Reporting::AttackResult]
|
238
246
|
def update_successful_attack_response context, ia_result, result, attack_string = nil
|
247
|
+
cef_outcome = :successful_attack
|
239
248
|
case mode
|
240
249
|
when :MONITOR
|
241
250
|
# We are checking the result as the ia_result would not contain the sub-rules.
|
242
251
|
result.response = if SUSPICIOUS_REPORTING_RULES.include?(result&.rule_id)
|
252
|
+
cef_outcome = :suspicious_attack
|
243
253
|
Contrast::Agent::Reporting::ResponseType::SUSPICIOUS
|
244
254
|
else
|
245
255
|
Contrast::Agent::Reporting::ResponseType::MONITORED
|
@@ -250,7 +260,11 @@ module Contrast
|
|
250
260
|
|
251
261
|
ia_result.attack_count = ia_result.attack_count + 1 if ia_result
|
252
262
|
log_rule_matched(context, ia_result, result.response, attack_string)
|
253
|
-
|
263
|
+
cef_logging(result,
|
264
|
+
cef_outcome,
|
265
|
+
value: ia_result&.value || attack_string,
|
266
|
+
input_type: ia_result&.input_type,
|
267
|
+
context: context)
|
254
268
|
result
|
255
269
|
end
|
256
270
|
|
@@ -265,17 +279,32 @@ module Contrast
|
|
265
279
|
# multiple inputs being found to violate the protection criteria
|
266
280
|
# @return [Contrast::Agent::Reporting::AttackResult]
|
267
281
|
def update_perimeter_attack_response context, ia_result, result
|
268
|
-
|
282
|
+
cef_outcome = :successful_attack
|
283
|
+
case mode
|
284
|
+
when :BLOCK_AT_PERIMETER
|
269
285
|
result.response = if blocked_rule?(ia_result)
|
270
286
|
Contrast::Agent::Reporting::ResponseType::BLOCKED
|
271
287
|
else
|
272
288
|
Contrast::Agent::Reporting::ResponseType::BLOCKED_AT_PERIMETER
|
273
289
|
end
|
274
290
|
log_rule_matched(context, ia_result, result.response)
|
275
|
-
|
291
|
+
when :BLOCK && rule_name == Contrast::Agent::Protect::Rule::Xss::NAME
|
292
|
+
# Handle cases like reflected-xss:
|
293
|
+
result.response = Contrast::Agent::Reporting::ResponseType::BLOCKED
|
294
|
+
log_rule_matched(context, ia_result, result.response)
|
295
|
+
else
|
296
|
+
# Handles all other cases including Reflected-xss in MONITOR mode.
|
297
|
+
return unless ia_result.nil? || ia_result.attack_count.zero?
|
298
|
+
|
276
299
|
result.response = assign_reporter_response_type(ia_result)
|
300
|
+
cef_outcome = suspicious_rule?(ia_result) ? :suspicious_attack : :ineffective_attack
|
277
301
|
log_rule_probed(context, ia_result)
|
278
302
|
end
|
303
|
+
cef_logging(result,
|
304
|
+
cef_outcome,
|
305
|
+
value: ia_result&.value,
|
306
|
+
input_type: ia_result&.input_type,
|
307
|
+
context: context)
|
279
308
|
|
280
309
|
result
|
281
310
|
end
|
@@ -334,7 +363,7 @@ module Contrast
|
|
334
363
|
# the rule id.
|
335
364
|
#
|
336
365
|
# @param context [Contrast::Agent::RequestContext]
|
337
|
-
# @return [Array<Contrast::Agent::Reporting::
|
366
|
+
# @return [Array<Contrast::Agent::Reporting::InputAnalysisResult>]
|
338
367
|
def gather_ia_results context
|
339
368
|
return [] unless context&.agent_input_analysis&.results
|
340
369
|
|
@@ -349,7 +378,8 @@ module Contrast
|
|
349
378
|
def blocked_violation? result
|
350
379
|
return false unless result
|
351
380
|
|
352
|
-
result.response == Contrast::Agent::Reporting::ResponseType::BLOCKED
|
381
|
+
blocked? && (result.response == Contrast::Agent::Reporting::ResponseType::BLOCKED ||
|
382
|
+
result.response == Contrast::Agent::Reporting::ResponseType::BLOCKED_AT_PERIMETER)
|
353
383
|
end
|
354
384
|
|
355
385
|
private
|
@@ -359,7 +389,8 @@ module Contrast
|
|
359
389
|
def blocked_rule? ia_result
|
360
390
|
[
|
361
391
|
Contrast::Agent::Protect::Rule::Sqli::NAME,
|
362
|
-
Contrast::Agent::Protect::Rule::NoSqli::NAME
|
392
|
+
Contrast::Agent::Protect::Rule::NoSqli::NAME,
|
393
|
+
Contrast::Agent::Protect::Rule::Xss::NAME
|
363
394
|
].include?(ia_result&.rule_id)
|
364
395
|
end
|
365
396
|
|
@@ -392,7 +423,7 @@ module Contrast
|
|
392
423
|
|
393
424
|
# @param context [Contrast::Agent::RequestContext]
|
394
425
|
# @param potential_attack_string [String, nil]
|
395
|
-
# @return [Contrast::Agent::Reporting, nil]
|
426
|
+
# @return [Contrast::Agent::Reporting::AttackResult, nil]
|
396
427
|
def find_postfilter_attacker context, potential_attack_string, **kwargs
|
397
428
|
ia_results = gather_ia_results(context)
|
398
429
|
ia_results.select! do |ia_result|
|
@@ -19,6 +19,7 @@ module Contrast
|
|
19
19
|
|
20
20
|
NAME = 'bot-blocker'
|
21
21
|
APPLICABLE_USER_INPUTS = [HEADER].cs__freeze
|
22
|
+
BLOCK_MESSAGE = 'Bot Blocker rule triggered. Unsafe Bot blocked.'
|
22
23
|
|
23
24
|
def rule_name
|
24
25
|
NAME
|
@@ -28,6 +29,13 @@ module Contrast
|
|
28
29
|
APPLICABLE_USER_INPUTS
|
29
30
|
end
|
30
31
|
|
32
|
+
# Return the specific blocking message for this rule.
|
33
|
+
#
|
34
|
+
# @return [String] the reason for the raised security exception.
|
35
|
+
def block_message
|
36
|
+
BLOCK_MESSAGE
|
37
|
+
end
|
38
|
+
|
31
39
|
# Bot blocker input classification
|
32
40
|
#
|
33
41
|
# @return [module<Contrast::Agent::Protect::Rule::BotBlockerInputClassification>]
|
@@ -49,13 +57,13 @@ module Contrast
|
|
49
57
|
ia_result.score_level == Contrast::Agent::Reporting::ScoreLevel::DEFINITEATTACK
|
50
58
|
|
51
59
|
result = build_attack_without_match(context, ia_result, nil)
|
52
|
-
|
53
|
-
cef_logging(result, :successful_attack) if result
|
54
|
-
return unless blocked?
|
60
|
+
return unless result
|
55
61
|
|
62
|
+
append_to_activity(context, result)
|
63
|
+
record_triggered(context)
|
56
64
|
# Raise BotBlocker error
|
57
65
|
exception_message = "#{ rule_name } rule triggered. Unsafe Bot blocked."
|
58
|
-
raise(Contrast::SecurityException.new(self, exception_message))
|
66
|
+
raise(Contrast::SecurityException.new(self, exception_message)) if blocked_violation?(result)
|
59
67
|
end
|
60
68
|
|
61
69
|
# @param context [Contrast::Agent::RequestContext]
|
@@ -22,32 +22,6 @@ module Contrast
|
|
22
22
|
class << self
|
23
23
|
include Contrast::Agent::Protect::Rule::InputClassification::Base
|
24
24
|
|
25
|
-
# Input Classification stage is done to determine if an user input is
|
26
|
-
# DEFINITEATTACK or to be ignored.
|
27
|
-
#
|
28
|
-
# @param rule_id [String] Name of the protect rule.
|
29
|
-
# @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
|
30
|
-
# @param value [Hash<String>] the value of the input.
|
31
|
-
# @param input_analysis [Contrast::Agent::Reporting::InputAnalysis] Holds all the results from the
|
32
|
-
# agent analysis from the current
|
33
|
-
# Request.
|
34
|
-
# @return ia [Contrast::Agent::Reporting::InputAnalysis, nil] with updated results.
|
35
|
-
def classify rule_id, input_type, value, input_analysis
|
36
|
-
return unless (rule = Contrast::PROTECT.rule(rule_id))
|
37
|
-
return unless rule.applicable_user_inputs.include?(input_type)
|
38
|
-
return unless input_analysis.request
|
39
|
-
|
40
|
-
value.each_value do |val|
|
41
|
-
result = create_new_input_result(input_analysis.request, rule.rule_name, input_type, val)
|
42
|
-
append_result(input_analysis, result)
|
43
|
-
end
|
44
|
-
|
45
|
-
input_analysis
|
46
|
-
rescue StandardError => e
|
47
|
-
logger.debug("An Error was recorded in the input classification of the #{ rule_id }", error: e)
|
48
|
-
nil
|
49
|
-
end
|
50
|
-
|
51
25
|
private
|
52
26
|
|
53
27
|
# This methods checks if input is tagged WORTHWATCHING or IGNORE matches value with it's
|
@@ -84,12 +84,9 @@ module Contrast
|
|
84
84
|
return unless result
|
85
85
|
|
86
86
|
append_to_activity(context, result)
|
87
|
-
|
88
|
-
|
89
|
-
return unless blocked?
|
90
|
-
|
87
|
+
record_triggered(context)
|
91
88
|
# Raise cmdi error
|
92
|
-
raise_error(classname, method)
|
89
|
+
raise_error(classname, method) if blocked_violation?(result)
|
93
90
|
end
|
94
91
|
end
|
95
92
|
end
|
@@ -43,10 +43,8 @@ module Contrast
|
|
43
43
|
**{ classname: classname, method: method }))
|
44
44
|
|
45
45
|
append_to_activity(context, result)
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
raise_error(classname, method)
|
46
|
+
record_triggered(context)
|
47
|
+
raise_error(classname, method) if blocked_violation?(result)
|
50
48
|
end
|
51
49
|
|
52
50
|
private
|
@@ -20,7 +20,7 @@ module Contrast
|
|
20
20
|
APPLICABLE_USER_INPUTS = [
|
21
21
|
BODY, COOKIE_VALUE, HEADER, PARAMETER_NAME,
|
22
22
|
PARAMETER_VALUE, JSON_VALUE, MULTIPART_VALUE,
|
23
|
-
MULTIPART_FIELD_NAME, XML_VALUE, DWR_VALUE
|
23
|
+
MULTIPART_FIELD_NAME, XML_VALUE, DWR_VALUE, UNKNOWN
|
24
24
|
].cs__freeze
|
25
25
|
|
26
26
|
# CMDI input classification
|
@@ -46,6 +46,7 @@ module Contrast
|
|
46
46
|
return unless (result = build_violation(context, command))
|
47
47
|
|
48
48
|
append_to_activity(context, result)
|
49
|
+
record_triggered(context)
|
49
50
|
raise_error(classname, method) if blocked_violation?(result)
|
50
51
|
end
|
51
52
|
|
@@ -50,6 +50,7 @@ module Contrast
|
|
50
50
|
end
|
51
51
|
|
52
52
|
# Return the specific blocking message for this rule.
|
53
|
+
#
|
53
54
|
# @return [String] the reason for the raised security exception.
|
54
55
|
def block_message
|
55
56
|
BLOCK_MESSAGE
|
@@ -84,10 +85,9 @@ module Contrast
|
|
84
85
|
kwargs = { GADGET_TYPE: gadget }
|
85
86
|
result = build_attack_with_match(context, ia_result, nil, serialized_input, **kwargs)
|
86
87
|
append_to_activity(context, result)
|
88
|
+
record_triggered(context)
|
87
89
|
|
88
|
-
|
89
|
-
|
90
|
-
raise(Contrast::SecurityException.new(self, block_message)) if blocked?
|
90
|
+
raise(Contrast::SecurityException.new(self, block_message)) if blocked_violation?(result)
|
91
91
|
end
|
92
92
|
|
93
93
|
# Determine if the issued command was called while we're
|
@@ -106,13 +106,13 @@ module Contrast
|
|
106
106
|
ia_result = build_evaluation(gadget_command)
|
107
107
|
result = build_attack_with_match(context, ia_result, nil, gadget_command, **kwargs)
|
108
108
|
append_to_activity(context, result)
|
109
|
-
cef_logging(result, :successful_attack, value: gadget_command)
|
110
109
|
raise(Contrast::SecurityException.new(self, BLOCK_MESSAGE)) if blocked?
|
111
110
|
end
|
112
111
|
|
113
112
|
protected
|
114
113
|
|
115
114
|
# Build the RaspRuleSample for the detected Deserialization attack.
|
115
|
+
#
|
116
116
|
# @param context [Contrast::Agent::RequestContext] the request
|
117
117
|
# context in which this attack is occurring.
|
118
118
|
# @param input_analysis_result [Contrast::Agent::Reporting::InputAnalysis]
|
@@ -24,7 +24,7 @@ module Contrast
|
|
24
24
|
COOKIE_VALUE, PARAMETER_VALUE, HEADER, JSON_VALUE, MULTIPART_VALUE, XML_VALUE, DWR_VALUE
|
25
25
|
].cs__freeze
|
26
26
|
|
27
|
-
BASE64_INPUT_TYPES = [BODY, COOKIE_VALUE, PARAMETER_VALUE, MULTIPART_VALUE, XML_VALUE].cs__freeze
|
27
|
+
BASE64_INPUT_TYPES = [BODY, HEADER, COOKIE_VALUE, PARAMETER_VALUE, MULTIPART_VALUE, XML_VALUE].cs__freeze
|
28
28
|
|
29
29
|
class << self
|
30
30
|
include Contrast::Components::Logger::InstanceMethods
|
@@ -183,9 +183,6 @@ module Contrast
|
|
183
183
|
return value unless Contrast::PROTECT.normalize_base64?
|
184
184
|
return value unless BASE64_INPUT_TYPES.include?(input_type)
|
185
185
|
|
186
|
-
# TODO: RUBY-2110 Update the HEADER handling if possible.
|
187
|
-
# We need only the Header values.
|
188
|
-
|
189
186
|
cs__decode64(value, input_type)
|
190
187
|
end
|
191
188
|
end
|