contrast-agent 7.3.2 → 7.4.1
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/middleware/middleware.rb +1 -1
- data/lib/contrast/agent/protect/input_analyzer/input_analyzer.rb +9 -11
- 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 +61 -26
- data/lib/contrast/agent/protect/rule/bot_blocker/bot_blocker.rb +12 -4
- data/lib/contrast/agent/protect/rule/cmdi/cmd_injection.rb +19 -15
- 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 +7 -2
- data/lib/contrast/agent/protect/rule/input_classification/encoding.rb +1 -1
- data/lib/contrast/agent/protect/rule/no_sqli/no_sqli.rb +5 -2
- data/lib/contrast/agent/protect/rule/path_traversal/path_traversal.rb +20 -8
- data/lib/contrast/agent/protect/rule/path_traversal/path_traversal_semantic_security_bypass.rb +2 -2
- data/lib/contrast/agent/protect/rule/sqli/sqli.rb +8 -1
- 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/protect/state.rb +110 -0
- 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/reporting_events/application_defend_attack_sample_activity.rb +2 -0
- data/lib/contrast/agent/reporting/reporting_events/architecture_component.rb +2 -0
- data/lib/contrast/agent/reporting/reporting_events/finding.rb +1 -4
- data/lib/contrast/agent/reporting/reporting_events/finding_event.rb +4 -0
- data/lib/contrast/agent/reporting/reporting_events/finding_request.rb +2 -0
- data/lib/contrast/agent/reporting/reporting_events/observed_library_usage.rb +2 -0
- data/lib/contrast/agent/reporting/reporting_events/preflight_message.rb +9 -8
- data/lib/contrast/agent/reporting/reporting_events/reportable_hash.rb +30 -6
- data/lib/contrast/agent/reporting/reporting_utilities/reporter_client.rb +1 -1
- data/lib/contrast/agent/reporting/reporting_utilities/resend.rb +1 -1
- data/lib/contrast/agent/reporting/reporting_utilities/response_handler_utils.rb +1 -5
- data/lib/contrast/agent/reporting/settings/protect.rb +3 -3
- data/lib/contrast/agent/reporting/settings/sampling.rb +5 -4
- data/lib/contrast/agent/request/request_context_extend.rb +0 -2
- data/lib/contrast/agent/version.rb +1 -1
- data/lib/contrast/components/agent.rb +3 -5
- data/lib/contrast/components/api.rb +3 -3
- data/lib/contrast/components/assess.rb +4 -0
- data/lib/contrast/components/assess_rules.rb +1 -2
- data/lib/contrast/components/base.rb +1 -2
- data/lib/contrast/components/config/sources.rb +23 -0
- data/lib/contrast/components/logger.rb +19 -0
- data/lib/contrast/components/protect.rb +55 -14
- data/lib/contrast/components/sampling.rb +5 -12
- data/lib/contrast/components/security_logger.rb +17 -0
- data/lib/contrast/components/settings.rb +110 -76
- data/lib/contrast/config/certification_configuration.rb +1 -1
- data/lib/contrast/config/configuration_files.rb +0 -2
- data/lib/contrast/config/diagnostics/config.rb +3 -3
- data/lib/contrast/config/diagnostics/effective_config.rb +1 -1
- data/lib/contrast/config/diagnostics/environment_variables.rb +21 -11
- data/lib/contrast/config/diagnostics/monitor.rb +1 -1
- data/lib/contrast/config/diagnostics/singleton_tools.rb +170 -0
- data/lib/contrast/config/diagnostics/source_config_value.rb +14 -9
- data/lib/contrast/config/diagnostics/tools.rb +23 -84
- data/lib/contrast/config/request_audit_configuration.rb +1 -1
- data/lib/contrast/config/server_configuration.rb +3 -15
- data/lib/contrast/configuration.rb +5 -2
- data/lib/contrast/framework/manager.rb +4 -3
- data/lib/contrast/framework/manager_extend.rb +3 -1
- data/lib/contrast/framework/rack/support.rb +11 -2
- 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/log_utils.rb +22 -11
- data/lib/contrast/utils/request_utils.rb +1 -1
- data/lib/contrast/utils/timer.rb +1 -1
- metadata +4 -2
@@ -32,6 +32,9 @@ module Contrast
|
|
32
32
|
NAME
|
33
33
|
end
|
34
34
|
|
35
|
+
# Return the specific blocking message for this rule.
|
36
|
+
#
|
37
|
+
# @return [String] the reason for the raised security exception.
|
35
38
|
def block_message
|
36
39
|
BLOCK_MESSAGE
|
37
40
|
end
|
@@ -56,9 +59,9 @@ module Contrast
|
|
56
59
|
return unless result
|
57
60
|
|
58
61
|
append_to_activity(context, result)
|
59
|
-
|
62
|
+
record_triggered(context)
|
60
63
|
cef_logging(result, :successful_attack)
|
61
|
-
raise(Contrast::SecurityException.new(self, BLOCK_MESSAGE)) if
|
64
|
+
raise(Contrast::SecurityException.new(self, BLOCK_MESSAGE)) if blocked_violation?(result)
|
62
65
|
end
|
63
66
|
|
64
67
|
def build_attack_with_match context, input_analysis_result, result, candidate_string, **kwargs
|
@@ -26,21 +26,37 @@ module Contrast
|
|
26
26
|
JSON_VALUE, MULTIPART_VALUE, MULTIPART_FIELD_NAME, XML_VALUE, DWR_VALUE, URI
|
27
27
|
].cs__freeze
|
28
28
|
|
29
|
+
BLOCK_MESSAGE = 'Path Traversal rule triggered. Request blocked.'
|
30
|
+
|
29
31
|
def rule_name
|
30
32
|
NAME
|
31
33
|
end
|
32
34
|
|
35
|
+
# Sub-rules forwarders:
|
36
|
+
|
37
|
+
# @return [Contrast::Agent::Protect::Rule::PathTraversalSemanticBypass]
|
38
|
+
def semantic_file_security_bypass
|
39
|
+
@_semantic_file_security_bypass ||= Contrast::Agent::Protect::Rule::PathTraversalSemanticBypass.new
|
40
|
+
end
|
41
|
+
|
33
42
|
# Array of sub_rules
|
34
43
|
#
|
35
44
|
# @return [Array]
|
36
45
|
def sub_rules
|
37
|
-
@_sub_rules ||= [
|
46
|
+
@_sub_rules ||= [semantic_file_security_bypass].cs__freeze
|
38
47
|
end
|
39
48
|
|
40
49
|
def applicable_user_inputs
|
41
50
|
APPLICABLE_USER_INPUTS
|
42
51
|
end
|
43
52
|
|
53
|
+
# Return the specific blocking message for this rule.
|
54
|
+
#
|
55
|
+
# @return [String] the reason for the raised security exception.
|
56
|
+
def block_message
|
57
|
+
BLOCK_MESSAGE
|
58
|
+
end
|
59
|
+
|
44
60
|
# Path Traversal input classification
|
45
61
|
#
|
46
62
|
# @return [module<Contrast::Agent::Protect::Rule::PathTraversalInputClassification>]
|
@@ -48,19 +64,15 @@ module Contrast
|
|
48
64
|
@_classification ||= Contrast::Agent::Protect::Rule::PathTraversalInputClassification.cs__freeze
|
49
65
|
end
|
50
66
|
|
51
|
-
def infilter context,
|
67
|
+
def infilter context, _method, path
|
52
68
|
return unless infilter?(context)
|
53
69
|
|
54
70
|
result = find_attacker(context, path)
|
55
71
|
return unless result
|
56
72
|
|
57
73
|
append_to_activity(context, result)
|
58
|
-
|
59
|
-
|
60
|
-
result_rule_name = Contrast::Utils::StringUtils.transform_string(result.rule_id)
|
61
|
-
cef_logging(result, :successful_attack, value: path)
|
62
|
-
exception_messasge = "#{ result_rule_name } rule triggered. Call to File.#{ method } blocked."
|
63
|
-
raise(Contrast::SecurityException.new(self, exception_messasge))
|
74
|
+
record_triggered(context)
|
75
|
+
raise(Contrast::SecurityException.new(self, block_message)) if blocked_violation?(result)
|
64
76
|
end
|
65
77
|
|
66
78
|
protected
|
data/lib/contrast/agent/protect/rule/path_traversal/path_traversal_semantic_security_bypass.rb
CHANGED
@@ -53,10 +53,10 @@ module Contrast
|
|
53
53
|
return unless result
|
54
54
|
|
55
55
|
append_to_activity(context, result)
|
56
|
-
|
56
|
+
record_triggered(context)
|
57
|
+
return unless blocked_violation?(result)
|
57
58
|
|
58
59
|
result_rule_name = Contrast::Utils::StringUtils.transform_string(result.rule_id)
|
59
|
-
cef_logging(result, :successful_attack, value: path)
|
60
60
|
exception_messasge = "#{ result_rule_name } rule triggered. Call to File.#{ method } blocked."
|
61
61
|
raise(Contrast::SecurityException.new(self, exception_messasge))
|
62
62
|
end
|
@@ -35,11 +35,18 @@ module Contrast
|
|
35
35
|
BLOCK_MESSAGE
|
36
36
|
end
|
37
37
|
|
38
|
+
# Sub-rules forwarders:
|
39
|
+
|
40
|
+
# @return [Contrast::Agent::Protect::Rule::SqliDangerousFunctions]
|
41
|
+
def semantic_dangerous_functions
|
42
|
+
@_semantic_dangerous_functions ||= Contrast::Agent::Protect::Rule::SqliDangerousFunctions.new
|
43
|
+
end
|
44
|
+
|
38
45
|
# Array of sub_rules
|
39
46
|
#
|
40
47
|
# @return [Array]
|
41
48
|
def sub_rules
|
42
|
-
@_sub_rules ||= [
|
49
|
+
@_sub_rules ||= [semantic_dangerous_functions].cs__freeze
|
43
50
|
end
|
44
51
|
|
45
52
|
def applicable_user_inputs
|
@@ -26,9 +26,8 @@ module Contrast
|
|
26
26
|
return unless result
|
27
27
|
|
28
28
|
append_to_activity(context, result)
|
29
|
-
|
30
|
-
|
31
|
-
raise(Contrast::SecurityException.new(self, BLOCK_MESSAGE)) if blocked?
|
29
|
+
record_triggered(context)
|
30
|
+
raise(Contrast::SecurityException.new(self, BLOCK_MESSAGE)) if blocked_violation?(result)
|
32
31
|
end
|
33
32
|
end
|
34
33
|
end
|
@@ -26,9 +26,8 @@ module Contrast
|
|
26
26
|
return unless result
|
27
27
|
|
28
28
|
append_to_activity(context, result)
|
29
|
-
|
30
|
-
|
31
|
-
raise(Contrast::SecurityException.new(self, BLOCK_MESSAGE)) if blocked?
|
29
|
+
record_triggered(context)
|
30
|
+
raise(Contrast::SecurityException.new(self, BLOCK_MESSAGE)) if blocked_violation?(result)
|
32
31
|
end
|
33
32
|
|
34
33
|
protected
|
@@ -39,8 +38,8 @@ module Contrast
|
|
39
38
|
|
40
39
|
def build_violation context, potential_attack_string
|
41
40
|
result = build_attack_result(context)
|
42
|
-
update_successful_attack_response(context, nil, result, potential_attack_string)
|
43
41
|
append_sample(context, nil, result, potential_attack_string)
|
42
|
+
update_successful_attack_response(context, nil, result, potential_attack_string)
|
44
43
|
result
|
45
44
|
end
|
46
45
|
|
@@ -29,8 +29,8 @@ module Contrast
|
|
29
29
|
# this input
|
30
30
|
def build_attack_with_match context, ia_result, result, candidate_string, **kwargs
|
31
31
|
result ||= build_attack_result(context)
|
32
|
-
update_successful_attack_response(context, ia_result, result, candidate_string)
|
33
32
|
append_sample(context, ia_result, result, candidate_string, **kwargs)
|
33
|
+
update_successful_attack_response(context, ia_result, result, candidate_string)
|
34
34
|
|
35
35
|
result
|
36
36
|
end
|
@@ -53,8 +53,8 @@ module Contrast
|
|
53
53
|
# this input
|
54
54
|
def build_attack_without_match context, ia_result, result, **kwargs
|
55
55
|
result ||= build_attack_result(context)
|
56
|
-
update_perimeter_attack_response(context, ia_result, result)
|
57
56
|
append_sample(context, ia_result, result, nil, **kwargs)
|
57
|
+
update_perimeter_attack_response(context, ia_result, result)
|
58
58
|
|
59
59
|
result
|
60
60
|
end
|
@@ -97,11 +97,10 @@ module Contrast
|
|
97
97
|
# @param potential_attack_string [String]
|
98
98
|
def build_violation context, potential_attack_string
|
99
99
|
result = build_attack_result(context)
|
100
|
+
append_sample(context, nil, result, potential_attack_string)
|
100
101
|
update_successful_attack_response(context, nil, result, potential_attack_string)
|
101
102
|
return unless result
|
102
103
|
|
103
|
-
append_sample(context, nil, result, potential_attack_string)
|
104
|
-
cef_logging(result, :successful_attack)
|
105
104
|
result
|
106
105
|
end
|
107
106
|
end
|
@@ -25,11 +25,12 @@ module Contrast
|
|
25
25
|
|
26
26
|
ia_results.each do |ia_result|
|
27
27
|
result = build_attack_result(context)
|
28
|
-
build_attack_without_match(context, ia_result, result)
|
29
|
-
|
28
|
+
result = build_attack_without_match(context, ia_result, result)
|
29
|
+
next unless result
|
30
30
|
|
31
|
-
|
32
|
-
|
31
|
+
append_to_activity(context, result)
|
32
|
+
record_triggered(context)
|
33
|
+
raise(Contrast::SecurityException.new(self, block_message)) if blocked_violation?(result)
|
33
34
|
end
|
34
35
|
end
|
35
36
|
|
@@ -39,10 +40,10 @@ module Contrast
|
|
39
40
|
# @param context [Contrast::Agent::RequestContext]
|
40
41
|
# @return [Boolean]
|
41
42
|
def prefilter? context
|
42
|
-
return false unless context
|
43
43
|
return false unless enabled?
|
44
|
-
return false unless (results = gather_ia_results(context)) && results.any?
|
45
44
|
return false if protect_excluded_by_url?(rule_name)
|
45
|
+
return false unless context
|
46
|
+
return false unless (results = gather_ia_results(context)) && results.any?
|
46
47
|
return false if protect_excluded_by_input?(results)
|
47
48
|
|
48
49
|
true
|
@@ -52,13 +53,20 @@ module Contrast
|
|
52
53
|
# have a different implementation based on the rule. As such, there
|
53
54
|
# is not parent implementation.
|
54
55
|
#
|
55
|
-
# @param
|
56
|
+
# @param context [Contrast::Agent::RequestContext] the context for
|
56
57
|
# the current request
|
57
|
-
# @param
|
58
|
+
# @param match_string [String] the input that violated the rule and
|
58
59
|
# matched the attack detection logic
|
59
60
|
# @param _kwargs [Hash] key-value pairs used by the rule to build a
|
60
61
|
# report.
|
61
|
-
def infilter
|
62
|
+
def infilter context, match_string, _kwargs
|
63
|
+
return unless infilter?(context)
|
64
|
+
return unless (result = build_violation(context, match_string))
|
65
|
+
|
66
|
+
append_to_activity(context, result)
|
67
|
+
record_triggered(context)
|
68
|
+
raise(Contrast::SecurityException.new(self, block_message)) if blocked?
|
69
|
+
end
|
62
70
|
|
63
71
|
# Infilter check always called before infilter to check if the rule is infilter
|
64
72
|
# capable, not disabled or in other way excluded by url or input exclusions.
|
@@ -74,6 +82,18 @@ module Contrast
|
|
74
82
|
true
|
75
83
|
end
|
76
84
|
|
85
|
+
# Check befor commiting infilter
|
86
|
+
#
|
87
|
+
# @param context [Contrast::Agent::RequestContext]
|
88
|
+
def postfilter? context
|
89
|
+
return false unless enabled? && POSTFILTER_MODES.include?(mode)
|
90
|
+
return false if protect_excluded_by_url?(rule_name)
|
91
|
+
return false if protect_excluded_by_input?(gather_ia_results(context))
|
92
|
+
return false if mode == :NO_ACTION || mode == :PERMIT
|
93
|
+
|
94
|
+
true
|
95
|
+
end
|
96
|
+
|
77
97
|
# Actions required for the rules that have to happen after the
|
78
98
|
# application has completed its processing of the request.
|
79
99
|
#
|
@@ -88,18 +108,14 @@ module Contrast
|
|
88
108
|
# @param context [Contrast::Agent::RequestContext]
|
89
109
|
# @raise [Contrast::SecurityException]
|
90
110
|
def postfilter context
|
91
|
-
return unless
|
92
|
-
return false if protect_excluded_by_url?(rule_name)
|
93
|
-
return if protect_excluded_by_input?(gather_ia_results(context))
|
94
|
-
|
95
|
-
return if mode == :NO_ACTION || mode == :PERMIT
|
111
|
+
return unless postfilter?(context)
|
96
112
|
|
97
113
|
result = find_postfilter_attacker(context, nil)
|
98
114
|
return unless result&.samples&.any?
|
99
115
|
|
100
|
-
cef_logging(result)
|
101
116
|
append_to_activity(context, result)
|
102
|
-
|
117
|
+
record_triggered(context)
|
118
|
+
return unless blocked_violation?(result)
|
103
119
|
|
104
120
|
raise(Contrast::SecurityException.new(self, "#{ rule_name } triggered in postfilter. Response blocked."))
|
105
121
|
end
|
@@ -3,6 +3,8 @@
|
|
3
3
|
|
4
4
|
require 'contrast/agent/protect/rule/base'
|
5
5
|
require 'contrast/agent/protect/rule/xss/reflected_xss_input_classification'
|
6
|
+
require 'contrast/agent/reporting/details/xss_details'
|
7
|
+
require 'contrast/agent/reporting/details/xss_match'
|
6
8
|
require 'contrast/agent/reporting/input_analysis/input_type'
|
7
9
|
|
8
10
|
module Contrast
|
@@ -25,10 +27,56 @@ module Contrast
|
|
25
27
|
NAME
|
26
28
|
end
|
27
29
|
|
30
|
+
# Return the specific blocking message for this rule.
|
31
|
+
#
|
32
|
+
# @return [String] the reason for the raised security exception.
|
28
33
|
def block_message
|
29
34
|
BLOCK_MESSAGE
|
30
35
|
end
|
31
36
|
|
37
|
+
# Prefilter check always called before infilter to check if the rule is infilter
|
38
|
+
# capable, not disabled or in other way excluded by url or input exclusions.
|
39
|
+
#
|
40
|
+
# @param context [Contrast::Agent::RequestContext]
|
41
|
+
# @return [Boolean]
|
42
|
+
def prefilter? context
|
43
|
+
return false unless enabled?
|
44
|
+
return false if protect_excluded_by_url?(rule_name)
|
45
|
+
return false unless context
|
46
|
+
return false unless (results = gather_ia_results(context)) && results.any?
|
47
|
+
return false if protect_excluded_by_input?(results)
|
48
|
+
|
49
|
+
true
|
50
|
+
end
|
51
|
+
|
52
|
+
def prefilter context
|
53
|
+
return unless prefilter?(context)
|
54
|
+
|
55
|
+
ia_results = gather_ia_results(context)
|
56
|
+
|
57
|
+
ia_results.each do |ia_result|
|
58
|
+
result = build_attack_result(context)
|
59
|
+
result = build_attack_without_match(context, ia_result, result)
|
60
|
+
next unless result
|
61
|
+
|
62
|
+
append_to_activity(context, result)
|
63
|
+
# XSS is being triggered, so we need to add it to the triggered rules,
|
64
|
+
# So the IA won't be done for this rule again for the current request.
|
65
|
+
record_triggered(context)
|
66
|
+
raise(Contrast::SecurityException.new(self, block_message)) if blocked_violation?(result)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# XSS is evaluated only on prefilter
|
71
|
+
def infilter? _context
|
72
|
+
false
|
73
|
+
end
|
74
|
+
|
75
|
+
# XSS is evaluated only on prefilter
|
76
|
+
def postfilter? _context
|
77
|
+
false
|
78
|
+
end
|
79
|
+
|
32
80
|
# XSS Upload input classification
|
33
81
|
#
|
34
82
|
# @return [module<Contrast::Agent::Protect::Rule::ReflectedXssInputClassification>]
|
@@ -43,6 +91,38 @@ module Contrast
|
|
43
91
|
def applicable_user_inputs
|
44
92
|
APPLICABLE_USER_INPUTS
|
45
93
|
end
|
94
|
+
|
95
|
+
# Adding XSS details
|
96
|
+
#
|
97
|
+
# @param context [Contrast::Agent::RequestContext]
|
98
|
+
# @param ia_result [Contrast::Agent::Reporting::InputAnalysisResult]
|
99
|
+
# @param _xss_string
|
100
|
+
# @param **_kwargs
|
101
|
+
# @return [Contrast::Agent::Reporting::RaspRuleSample]
|
102
|
+
def build_sample context, ia_result, _xss_string, **_kwargs
|
103
|
+
sample = build_base_sample(context, ia_result)
|
104
|
+
sample.details = Contrast::Agent::Reporting::Details::XssDetails.new
|
105
|
+
sample.details.input = ia_result.value
|
106
|
+
|
107
|
+
# TODO: RUBY-99999 check the if the ReflectedXss matches are needed.
|
108
|
+
xss_match = Contrast::Agent::Reporting::Details::XssMatch.new(ia_result.value)
|
109
|
+
sample.details.matches << xss_match unless xss_match.empty?
|
110
|
+
|
111
|
+
sample
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
# @param context [Contrast::Agent::RequestContext]
|
117
|
+
# @param potential_attack_string [String, nil]
|
118
|
+
# @return [Contrast::Agent::Reporting::AttackResult, nil]
|
119
|
+
def find_postfilter_attacker context, potential_attack_string, **kwargs
|
120
|
+
ia_results = gather_ia_results(context)
|
121
|
+
ia_results.reject! do |ia_result|
|
122
|
+
ia_result.score_level == Contrast::Agent::Reporting::ScoreLevel::IGNORE
|
123
|
+
end
|
124
|
+
find_attacker_with_results(context, potential_attack_string, ia_results, **kwargs)
|
125
|
+
end
|
46
126
|
end
|
47
127
|
end
|
48
128
|
end
|
@@ -26,6 +26,13 @@ module Contrast
|
|
26
26
|
NAME
|
27
27
|
end
|
28
28
|
|
29
|
+
# Return the specific blocking message for this rule.
|
30
|
+
#
|
31
|
+
# @return [String] the reason for the raised security exception.
|
32
|
+
def block_message
|
33
|
+
BLOCK_MESSAGE
|
34
|
+
end
|
35
|
+
|
29
36
|
# Given an xml, evaluate it for an XXE attack. There's no return here
|
30
37
|
# as this method handles appending the evaluation to the request
|
31
38
|
# context, connecting it to the reporting mechanism at request end.
|
@@ -43,9 +50,9 @@ module Contrast
|
|
43
50
|
return unless result
|
44
51
|
|
45
52
|
append_to_activity(context, result)
|
46
|
-
|
53
|
+
record_triggered(context)
|
54
|
+
return unless blocked_violation?(result)
|
47
55
|
|
48
|
-
cef_logging(result, :successful_attack, value: xml)
|
49
56
|
raise(Contrast::SecurityException.new(self, BLOCK_MESSAGE))
|
50
57
|
end
|
51
58
|
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'contrast/utils/object_share'
|
5
|
+
require 'contrast/components/logger'
|
6
|
+
|
7
|
+
module Contrast
|
8
|
+
module Agent
|
9
|
+
module Protect
|
10
|
+
# Master class for each protect rule. This class will hold all the rules references.
|
11
|
+
# Any access to the rules should be done through this class. and new rules should be
|
12
|
+
# added here. Each main rule should require and include and initialize it's sub-rules.
|
13
|
+
class State
|
14
|
+
include Contrast::Components::Logger::InstanceMethods
|
15
|
+
|
16
|
+
# @return [boolean] State dictated by local or server settings
|
17
|
+
attr_accessor :enabled
|
18
|
+
# @return [Contrast::Agent::Protect::Rule::BotBlocker] the bot blocker rule
|
19
|
+
attr_reader :bot_blocker
|
20
|
+
# @return [Contrast::Agent::Protect::Rule::CmdInjection] the command injection rule
|
21
|
+
attr_reader :cmd_injection
|
22
|
+
# @return [Contrast::Agent::Protect::Rule::CmdiBackdoors]
|
23
|
+
attr_reader :cmd_injection_command_backdoors
|
24
|
+
# @return [Contrast::Agent::Protect::Rule::CmdiChainedCommand]
|
25
|
+
attr_reader :cmd_injection_semantic_chained_commands
|
26
|
+
# @return [Contrast::Agent::Protect::Rule::CmdiDangerousPath]
|
27
|
+
attr_reader :cmd_injection_semantic_dangerous_paths
|
28
|
+
# @return [Contrast::Agent::Protect::Rule::Deserialization]
|
29
|
+
attr_reader :untrusted_deserialization
|
30
|
+
# @return [Contrast::Agent::Protect::Rule::NoSqli]
|
31
|
+
attr_reader :nosql_injection
|
32
|
+
# @return [Contrast::Agent::Protect::Rule::PathTraversal]
|
33
|
+
attr_reader :path_traversal
|
34
|
+
# @return [Contrast::Agent::Protect::Rule::PathTraversalSemanticBypass]
|
35
|
+
attr_reader :path_traversal_semantic_file_security_bypass
|
36
|
+
# @return [Contrast::Agent::Protect::Rule::Sqli]
|
37
|
+
attr_reader :sql_injection
|
38
|
+
# @return [Contrast::Agent::Protect::Rule::SqliDangerousFunctions]
|
39
|
+
attr_reader :sql_injection_semantic_dangerous_functions
|
40
|
+
# @return [Contrast::Agent::Protect::Rule::UnsafeFileUpload] the unsafe file upload rule
|
41
|
+
attr_reader :unsafe_file_upload
|
42
|
+
# @return [Contrast::Agent::Protect::Rule::Xss] the reflected xss rule
|
43
|
+
attr_reader :reflected_xss
|
44
|
+
# @return [Contrast::Agent::Protect::Rule::Xxe] the xxe rule
|
45
|
+
attr_reader :xxe
|
46
|
+
|
47
|
+
# Initialize all the protect rules. This should be the one place to access each live
|
48
|
+
# rule reference.
|
49
|
+
def initialize
|
50
|
+
@bot_blocker = Contrast::Agent::Protect::Rule::BotBlocker.new
|
51
|
+
@cmd_injection = Contrast::Agent::Protect::Rule::CmdInjection.new
|
52
|
+
@cmd_injection_command_backdoors = @cmd_injection.command_backdoors
|
53
|
+
@cmd_injection_semantic_chained_commands = @cmd_injection.semantic_chained_commands
|
54
|
+
@cmd_injection_semantic_dangerous_paths = @cmd_injection.semantic_dangerous_paths
|
55
|
+
@untrusted_deserialization = Contrast::Agent::Protect::Rule::Deserialization.new
|
56
|
+
@nosql_injection = Contrast::Agent::Protect::Rule::NoSqli.new
|
57
|
+
@path_traversal = Contrast::Agent::Protect::Rule::PathTraversal.new
|
58
|
+
@path_traversal_semantic_file_security_bypass = @path_traversal.semantic_file_security_bypass
|
59
|
+
@sql_injection = Contrast::Agent::Protect::Rule::Sqli.new
|
60
|
+
@sql_injection_semantic_dangerous_functions = @sql_injection.semantic_dangerous_functions
|
61
|
+
@unsafe_file_upload = Contrast::Agent::Protect::Rule::UnsafeFileUpload.new
|
62
|
+
@reflected_xss = Contrast::Agent::Protect::Rule::Xss.new
|
63
|
+
@xxe = Contrast::Agent::Protect::Rule::Xxe.new
|
64
|
+
end
|
65
|
+
|
66
|
+
# Return the Rules in Hash form {rule_id => rule_class }.
|
67
|
+
# This is used to traverse for each rule and update it's settings.
|
68
|
+
# Also is the way a rule is retrieved given the ID is known.
|
69
|
+
#
|
70
|
+
# @return [Hash<String, Contrast::Agent::Protect::Rule::Base>]
|
71
|
+
def rules
|
72
|
+
@_rules ||= {
|
73
|
+
@bot_blocker.rule_name => @bot_blocker,
|
74
|
+
@cmd_injection.rule_name => @cmd_injection,
|
75
|
+
@cmd_injection_command_backdoors.rule_name => @cmd_injection_command_backdoors,
|
76
|
+
@cmd_injection_semantic_chained_commands.rule_name => @cmd_injection_semantic_chained_commands,
|
77
|
+
@cmd_injection_semantic_dangerous_paths.rule_name => @cmd_injection_semantic_dangerous_paths,
|
78
|
+
@untrusted_deserialization.rule_name => @untrusted_deserialization,
|
79
|
+
@nosql_injection.rule_name => @nosql_injection,
|
80
|
+
@path_traversal.rule_name => @path_traversal,
|
81
|
+
@path_traversal_semantic_file_security_bypass.rule_name => @path_traversal_semantic_file_security_bypass,
|
82
|
+
@sql_injection.rule_name => @sql_injection,
|
83
|
+
@sql_injection_semantic_dangerous_functions.rule_name => @sql_injection_semantic_dangerous_functions,
|
84
|
+
@unsafe_file_upload.rule_name => @unsafe_file_upload,
|
85
|
+
@reflected_xss.rule_name => @reflected_xss,
|
86
|
+
@xxe.rule_name => @xxe
|
87
|
+
}
|
88
|
+
end
|
89
|
+
|
90
|
+
# Update all settings from configuration.
|
91
|
+
def update
|
92
|
+
rules.values.each(&:update)
|
93
|
+
logger.info('Current rule settings:')
|
94
|
+
rules.each { |k, v| logger.info('Protect Rule mode set', rule: k, mode: v.mode) }
|
95
|
+
end
|
96
|
+
|
97
|
+
# Check the local configurations first then the server settings.
|
98
|
+
def enabled?
|
99
|
+
Contrast::PROTECT.enable || Contrast::SETTINGS.protect_state.enabled
|
100
|
+
end
|
101
|
+
|
102
|
+
# @param [String] rule_id
|
103
|
+
# @return [Contrast::Agent::Protect::Rule::Base]
|
104
|
+
def [] rule_id
|
105
|
+
rules[rule_id]
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -9,6 +9,9 @@ module Contrast
|
|
9
9
|
module Details
|
10
10
|
# Matcher data for XSS rule.
|
11
11
|
class XssMatch
|
12
|
+
EVIDENCE_START = /<script.*?>/i.cs__freeze
|
13
|
+
EVIDENCE_END = %r{</script.*?>}i.cs__freeze
|
14
|
+
|
12
15
|
# @return [Integer] in ms
|
13
16
|
attr_accessor :evidence_start
|
14
17
|
# @return [String]
|
@@ -16,6 +19,16 @@ module Contrast
|
|
16
19
|
# @return [Integer]
|
17
20
|
attr_accessor :offset
|
18
21
|
|
22
|
+
# @param xss_string [String] to check for matches.
|
23
|
+
def initialize xss_string = ''
|
24
|
+
return if xss_string.empty?
|
25
|
+
|
26
|
+
@evidence_start = xss_string.index(EVIDENCE_START)
|
27
|
+
@offset = (xss_string[0...@evidence_start] || '').length
|
28
|
+
@evidence = xss_string[@evidence_start...xss_string.index(EVIDENCE_END)].gsub(EVIDENCE_START, '').
|
29
|
+
gsub(EVIDENCE_END, '')
|
30
|
+
end
|
31
|
+
|
19
32
|
def to_controlled_hash
|
20
33
|
{
|
21
34
|
evidenceStart: evidence_start,
|
@@ -23,6 +36,10 @@ module Contrast
|
|
23
36
|
offset: offset
|
24
37
|
}
|
25
38
|
end
|
39
|
+
|
40
|
+
def empty?
|
41
|
+
evidence_start.nil? || evidence.nil? || offset.nil?
|
42
|
+
end
|
26
43
|
end
|
27
44
|
end
|
28
45
|
end
|
@@ -2,6 +2,7 @@
|
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
require 'contrast/utils/object_share'
|
5
|
+
require 'contrast/utils/duck_utils'
|
5
6
|
require 'contrast/agent/reporting/input_analysis/input_analysis_result'
|
6
7
|
|
7
8
|
module Contrast
|
@@ -12,10 +13,20 @@ module Contrast
|
|
12
13
|
# @return [Hash] Stored request inputs for this context.
|
13
14
|
attr_accessor :inputs
|
14
15
|
|
16
|
+
# Records rules triggered on sink.
|
17
|
+
#
|
18
|
+
# @return [Array<String>] array with rule names.
|
15
19
|
def triggered_rules
|
16
20
|
@_triggered_rules ||= []
|
17
21
|
end
|
18
22
|
|
23
|
+
# Records rules with input analysis for current request.
|
24
|
+
#
|
25
|
+
# @return [Array<String>] array with rule names.
|
26
|
+
def analysed_rules
|
27
|
+
@_analysed_rules ||= []
|
28
|
+
end
|
29
|
+
|
19
30
|
# result from input analysis
|
20
31
|
#
|
21
32
|
# @return @_results [Array<Contrast::Agent::Reporting::Settings::InputAnalysisResult>]
|
@@ -44,6 +55,27 @@ module Contrast
|
|
44
55
|
def request= request
|
45
56
|
@_request = request if request.instance_of?(Contrast::Agent::Request)
|
46
57
|
end
|
58
|
+
|
59
|
+
# Check if the InputAnalysis is empty.
|
60
|
+
def no_inputs?
|
61
|
+
Contrast::Utils::DuckUtils.empty_duck?(inputs)
|
62
|
+
end
|
63
|
+
|
64
|
+
# We add the analysed rules to the list. The IA won't be build for this rule,
|
65
|
+
# since already it's already available.
|
66
|
+
#
|
67
|
+
# @param rule_name [String] name to record.
|
68
|
+
def record_analysed_rule rule_name
|
69
|
+
analysed_rules << rule_name unless triggered_rules.include?(rule_name)
|
70
|
+
end
|
71
|
+
|
72
|
+
# We add the triggered rules to the list. After request analysis will skip this rule
|
73
|
+
# as already triggered.
|
74
|
+
#
|
75
|
+
# @param rule_name [String] name to record.
|
76
|
+
def record_rule_triggered rule_name
|
77
|
+
triggered_rules << rule_name unless triggered_rules.include?(rule_name)
|
78
|
+
end
|
47
79
|
end
|
48
80
|
end
|
49
81
|
end
|
@@ -119,15 +119,12 @@ module Contrast
|
|
119
119
|
ruleId: rule_id,
|
120
120
|
session_id: ::Contrast::ASSESS.session_id,
|
121
121
|
version: 4
|
122
|
-
}
|
122
|
+
}.compact
|
123
123
|
end
|
124
124
|
|
125
125
|
# @raise [ArgumentError]
|
126
126
|
def validate
|
127
127
|
raise(ArgumentError, "#{ self } did not have a proper rule. Unable to continue.") unless @rule_id
|
128
|
-
unless ::Contrast::ASSESS.session_id
|
129
|
-
raise(ArgumentError, "#{ self } did not have a proper session id. Unable to continue.")
|
130
|
-
end
|
131
128
|
if event_based? && events.empty?
|
132
129
|
raise(ArgumentError, "#{ self } did not have proper events for #{ @rule_id }. Unable to continue.")
|
133
130
|
end
|