contrast-agent 7.3.2 → 7.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|