contrast-agent 7.4.0 → 7.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
@@ -37,7 +37,7 @@ module Contrast
|
|
37
37
|
# The Base64 method will return printable ascii characters, so we can use this to determine if the value is
|
38
38
|
# encoded or not.
|
39
39
|
#
|
40
|
-
# The solution in this case is
|
40
|
+
# The solution in this case is encoding the value, and then decoding it. If the value is already encoded
|
41
41
|
# it will not be eq to the original value. If the value is not encoded, it will be eq to the original value.
|
42
42
|
#
|
43
43
|
# @param value [String] input to check for encoding status.
|
@@ -96,11 +96,43 @@ module Contrast
|
|
96
96
|
def cs__decode64 value, input_type
|
97
97
|
return value unless cs__base64?(value, input_type)
|
98
98
|
|
99
|
-
|
99
|
+
new_value = try_base64_decode(value)
|
100
|
+
new_value, success = normalize_encoding(new_value)
|
101
|
+
return new_value if success
|
102
|
+
|
103
|
+
value
|
100
104
|
rescue StandardError => e
|
101
105
|
logger.error('Error while decoding base64', error: e, message: e.message, backtrace: e.backtrace)
|
102
106
|
value
|
103
107
|
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
# Try and decode the value, do not use decoding if the value have zero bytes.
|
112
|
+
def try_base64_decode value
|
113
|
+
new_value = Base64.decode64(value)
|
114
|
+
# check for null bytes:
|
115
|
+
return new_value unless new_value.bytes.select(&:zero?).any?
|
116
|
+
|
117
|
+
value
|
118
|
+
end
|
119
|
+
|
120
|
+
# Detecting encoded Base64 is not perfect. In some cases it will detect certain inputs as
|
121
|
+
# encoded and will try to decode them. Even if the decoding is successful, the value may be
|
122
|
+
# encoded back to ASCII format. AgentLib will raise UTF-8 error in this case.
|
123
|
+
# This method will try to normalize the encoding to UTF-8. If the encoding fails, this means
|
124
|
+
# that the decoding was not successful and the value will be returned as is. Otherwise a
|
125
|
+
# base64 decoded string with ASCII-8BIT encoding will be parsed to UTF-8 without errors.
|
126
|
+
#
|
127
|
+
# @param value [String] input to normalize.
|
128
|
+
# @return [Array<String, Boolean>] normalized value and success flag.
|
129
|
+
def normalize_encoding value
|
130
|
+
new_value = value.dup.encode!('Windows-1252').force_encoding('UTF-8')
|
131
|
+
[new_value, true]
|
132
|
+
rescue StandardError
|
133
|
+
# encoding failed, or the decoding from base64 failed.
|
134
|
+
[nil, false]
|
135
|
+
end
|
104
136
|
end
|
105
137
|
end
|
106
138
|
end
|
@@ -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,6 +26,8 @@ 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
|
@@ -48,6 +50,13 @@ module Contrast
|
|
48
50
|
APPLICABLE_USER_INPUTS
|
49
51
|
end
|
50
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
|
+
|
51
60
|
# Path Traversal input classification
|
52
61
|
#
|
53
62
|
# @return [module<Contrast::Agent::Protect::Rule::PathTraversalInputClassification>]
|
@@ -55,19 +64,15 @@ module Contrast
|
|
55
64
|
@_classification ||= Contrast::Agent::Protect::Rule::PathTraversalInputClassification.cs__freeze
|
56
65
|
end
|
57
66
|
|
58
|
-
def infilter context,
|
67
|
+
def infilter context, _method, path
|
59
68
|
return unless infilter?(context)
|
60
69
|
|
61
70
|
result = find_attacker(context, path)
|
62
71
|
return unless result
|
63
72
|
|
64
73
|
append_to_activity(context, result)
|
65
|
-
|
66
|
-
|
67
|
-
result_rule_name = Contrast::Utils::StringUtils.transform_string(result.rule_id)
|
68
|
-
cef_logging(result, :successful_attack, value: path)
|
69
|
-
exception_messasge = "#{ result_rule_name } rule triggered. Call to File.#{ method } blocked."
|
70
|
-
raise(Contrast::SecurityException.new(self, exception_messasge))
|
74
|
+
record_triggered(context)
|
75
|
+
raise(Contrast::SecurityException.new(self, block_message)) if blocked_violation?(result)
|
71
76
|
end
|
72
77
|
|
73
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
|
@@ -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
|
|
@@ -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
|
@@ -33,42 +33,12 @@ module Contrast
|
|
33
33
|
# @return
|
34
34
|
def to_a
|
35
35
|
@_to_a ||= [
|
36
|
-
UNDEFINED_TYPE, BODY, COOKIE_NAME, COOKIE_VALUE, HEADER, PARAMETER_NAME,
|
37
|
-
QUERYSTRING, URI, SOCKET, JSON_VALUE, JSON_ARRAYED_VALUE, MULTIPART_CONTENT_TYPE,
|
38
|
-
MULTIPART_FIELD_NAME, MULTIPART_NAME, XML_VALUE, DWR_VALUE, METHOD, REQUEST,
|
36
|
+
UNDEFINED_TYPE, BODY, COOKIE_NAME, COOKIE_VALUE, HEADER, PARAMETER_NAME,
|
37
|
+
PARAMETER_VALUE, QUERYSTRING, URI, SOCKET, JSON_VALUE, JSON_ARRAYED_VALUE, MULTIPART_CONTENT_TYPE,
|
38
|
+
MULTIPART_VALUE, MULTIPART_FIELD_NAME, MULTIPART_NAME, XML_VALUE, DWR_VALUE, METHOD, REQUEST,
|
39
|
+
URL_PARAMETER, UNKNOWN
|
39
40
|
]
|
40
41
|
end
|
41
|
-
|
42
|
-
# This is a hash of the input types and their corresponding values.
|
43
|
-
#
|
44
|
-
# @return [Hash]
|
45
|
-
|
46
|
-
def to_hash
|
47
|
-
{
|
48
|
-
UNDEFINED_TYPE: '1',
|
49
|
-
BODY: '2',
|
50
|
-
COOKIE_NAME: '3',
|
51
|
-
COOKIE_VALUE: '4',
|
52
|
-
HEADER: '5',
|
53
|
-
PARAMETER_NAME: '6',
|
54
|
-
PARAMETER_VALUE: '7',
|
55
|
-
QUERYSTRING: '8',
|
56
|
-
URI: '9',
|
57
|
-
SOCKET: '10',
|
58
|
-
JSON_VALUE: '11',
|
59
|
-
JSON_ARRAYED_VALUE: '12',
|
60
|
-
MULTIPART_CONTENT_TYPE: '13',
|
61
|
-
MULTIPART_VALUE: '14',
|
62
|
-
MULTIPART_FIELD_NAME: '15',
|
63
|
-
MULTIPART_NAME: '16',
|
64
|
-
XML_VALUE: '17',
|
65
|
-
DWR_VALUE: '18',
|
66
|
-
METHOD: '19',
|
67
|
-
REQUEST: '20',
|
68
|
-
URL_PARAMETER: '21',
|
69
|
-
UNKNOWN: '22'
|
70
|
-
}
|
71
|
-
end
|
72
42
|
end
|
73
43
|
end
|
74
44
|
end
|
@@ -119,16 +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
|
-
|
129
|
-
unless ::Contrast::ASSESS.session_id
|
130
|
-
raise(ArgumentError, "#{ self } did not have a proper session id. Unable to continue.")
|
131
|
-
end
|
132
128
|
if event_based? && events.empty?
|
133
129
|
raise(ArgumentError, "#{ self } did not have proper events for #{ @rule_id }. Unable to continue.")
|
134
130
|
end
|
@@ -43,11 +43,11 @@ module Contrast
|
|
43
43
|
appPath: ::Contrast::APP_CONTEXT.name, # rubocop:disable Security/Module/Name
|
44
44
|
appVersion: ::Contrast::APP_CONTEXT.version,
|
45
45
|
code: CODE,
|
46
|
-
data: '',
|
46
|
+
data: @data || '',
|
47
47
|
key: 0,
|
48
48
|
session_id: ::Contrast::ASSESS.session_id,
|
49
49
|
routes: @routes.map(&:to_controlled_hash)
|
50
|
-
}
|
50
|
+
}.compact
|
51
51
|
end
|
52
52
|
|
53
53
|
# @raise [ArgumentError]
|
@@ -55,9 +55,6 @@ module Contrast
|
|
55
55
|
unless Contrast::Utils::StringUtils.present?(data)
|
56
56
|
raise(ArgumentError, "#{ cs__class } did not have a proper data. Unable to continue.")
|
57
57
|
end
|
58
|
-
unless ::Contrast::ASSESS.session_id
|
59
|
-
raise(ArgumentError, "#{ cs__class } did not have a proper session id. Unable to continue.")
|
60
|
-
end
|
61
58
|
unless Contrast::APP_CONTEXT.name # rubocop:disable Security/Module/Name
|
62
59
|
raise(ArgumentError, "#{ cs__class } did not have a proper Application Name. Unable to continue.")
|
63
60
|
end
|
@@ -17,14 +17,14 @@ module Contrast
|
|
17
17
|
# @param finding [Contrast::Agent::Reporting::Finding]
|
18
18
|
# @return [Contrast::Agent::Reporting::Preflight, nil]
|
19
19
|
def generate finding
|
20
|
-
return unless finding
|
20
|
+
return unless finding&.cs__is_a?(Contrast::Agent::Reporting::Finding)
|
21
21
|
|
22
22
|
new_preflight = Contrast::Agent::Reporting::Preflight.new
|
23
23
|
new_preflight_message = Contrast::Agent::Reporting::PreflightMessage.new
|
24
|
-
finding.routes
|
25
|
-
|
24
|
+
routes = finding.routes
|
25
|
+
unless Contrast::Utils::DuckUtils.empty_duck?(routes)
|
26
|
+
routes.each { |route| new_preflight_message.routes << route }
|
26
27
|
end
|
27
|
-
new_preflight_message.hash_code = finding.hash_code
|
28
28
|
new_preflight_message.data = "#{ finding.rule_id },#{ finding.hash_code }"
|
29
29
|
new_preflight.messages << new_preflight_message
|
30
30
|
return new_preflight unless Contrast::Utils::DuckUtils.empty_duck?(new_preflight.messages)
|
@@ -118,7 +118,11 @@ module Contrast
|
|
118
118
|
mode.resend.reset_rescue_attempts
|
119
119
|
findings_to_return.each do |index|
|
120
120
|
preflight_message = event.messages[index.to_i]
|
121
|
-
|
121
|
+
preflight_data = preflight_message&.data
|
122
|
+
corresponding_finding = Contrast::Agent::Reporting::ReportingStorage.delete(preflight_data)
|
123
|
+
if Contrast::Agent::REQUEST_TRACKER.current
|
124
|
+
Contrast::Agent::REQUEST_TRACKER.current.reported_findings << preflight_data
|
125
|
+
end
|
122
126
|
next unless corresponding_finding
|
123
127
|
|
124
128
|
send_event(corresponding_finding, connection)
|
@@ -263,7 +263,7 @@ module Contrast
|
|
263
263
|
extract_response_last_modified(response, event)
|
264
264
|
populate_response(response_data, event)
|
265
265
|
rescue StandardError => e
|
266
|
-
logger.error('Unable to convert response', e)
|
266
|
+
logger.error('Unable to convert response', error: e)
|
267
267
|
nil
|
268
268
|
end
|
269
269
|
|