contrast-agent 7.2.0 → 7.3.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/assess/policy/policy_node.rb +25 -6
- data/lib/contrast/agent/assess/policy/propagator/response.rb +64 -0
- data/lib/contrast/agent/assess/policy/propagator.rb +1 -0
- data/lib/contrast/agent/assess/policy/source_method.rb +5 -0
- data/lib/contrast/agent/assess/rule/response/body_rule.rb +22 -7
- data/lib/contrast/agent/assess/rule/response/cache_control_header_rule.rb +4 -1
- data/lib/contrast/agent/protect/input_analyzer/input_analyzer.rb +62 -23
- data/lib/contrast/agent/protect/input_analyzer/worth_watching_analyzer.rb +37 -4
- data/lib/contrast/agent/protect/rule/base.rb +5 -1
- data/lib/contrast/agent/protect/rule/bot_blocker/bot_blocker_input_classification.rb +27 -11
- data/lib/contrast/agent/protect/rule/cmdi/cmdi_base_rule.rb +0 -1
- data/lib/contrast/agent/protect/rule/cmdi/cmdi_input_classification.rb +2 -2
- data/lib/contrast/agent/protect/rule/input_classification/base.rb +191 -0
- data/lib/contrast/agent/protect/rule/input_classification/base64_statistic.rb +71 -0
- data/lib/contrast/agent/protect/rule/input_classification/cached_result.rb +37 -0
- data/lib/contrast/agent/protect/rule/input_classification/encoding.rb +109 -0
- data/lib/contrast/agent/protect/rule/input_classification/encoding_rates.rb +47 -0
- data/lib/contrast/agent/protect/rule/input_classification/extendable.rb +80 -0
- data/lib/contrast/agent/protect/rule/input_classification/lru_cache.rb +198 -0
- data/lib/contrast/agent/protect/rule/input_classification/match_rates.rb +66 -0
- data/lib/contrast/agent/protect/rule/input_classification/rates.rb +53 -0
- data/lib/contrast/agent/protect/rule/input_classification/statistics.rb +115 -0
- data/lib/contrast/agent/protect/rule/input_classification/utils.rb +23 -0
- data/lib/contrast/agent/protect/rule/no_sqli/no_sqli_input_classification.rb +17 -7
- data/lib/contrast/agent/protect/rule/path_traversal/path_traversal_input_classification.rb +18 -15
- data/lib/contrast/agent/protect/rule/sqli/sqli_input_classification.rb +2 -2
- data/lib/contrast/agent/protect/rule/unsafe_file_upload/unsafe_file_upload_input_classification.rb +18 -15
- data/lib/contrast/agent/protect/rule/xss/reflected_xss_input_classification.rb +19 -17
- data/lib/contrast/agent/reporting/attack_result/attack_result.rb +6 -0
- data/lib/contrast/agent/reporting/input_analysis/input_analysis.rb +2 -7
- data/lib/contrast/agent/reporting/input_analysis/input_analysis_result.rb +11 -0
- data/lib/contrast/agent/reporting/input_analysis/input_type.rb +33 -1
- data/lib/contrast/agent/reporting/masker/masker_utils.rb +1 -1
- data/lib/contrast/agent/reporting/reporting_events/application_defend_activity.rb +1 -0
- data/lib/contrast/agent/reporting/reporting_events/application_defend_attacker_activity.rb +1 -0
- data/lib/contrast/agent/reporting/reporting_utilities/reporter_client_utils.rb +1 -1
- data/lib/contrast/agent/telemetry/base.rb +28 -2
- data/lib/contrast/agent/telemetry/base64_hash.rb +55 -0
- data/lib/contrast/agent/telemetry/cache_hash.rb +55 -0
- data/lib/contrast/agent/telemetry/client.rb +10 -2
- data/lib/contrast/agent/telemetry/exception/obfuscate.rb +4 -3
- data/lib/contrast/agent/telemetry/{hash.rb → exception_hash.rb} +1 -1
- data/lib/contrast/agent/telemetry/identifier.rb +13 -26
- data/lib/contrast/agent/telemetry/input_analysis_cache_event.rb +27 -0
- data/lib/contrast/agent/telemetry/input_analysis_encoding_event.rb +26 -0
- data/lib/contrast/agent/telemetry/input_analysis_event.rb +91 -0
- data/lib/contrast/agent/telemetry/metric_event.rb +12 -0
- data/lib/contrast/agent/telemetry/startup_metrics_event.rb +0 -8
- data/lib/contrast/agent/version.rb +1 -1
- data/lib/contrast/components/assess.rb +33 -6
- data/lib/contrast/components/base.rb +4 -2
- data/lib/contrast/components/config.rb +6 -6
- data/lib/contrast/components/protect.rb +11 -1
- data/lib/contrast/components/sampling.rb +15 -10
- data/lib/contrast/config/diagnostics/command_line.rb +2 -2
- data/lib/contrast/config/diagnostics/environment_variables.rb +5 -2
- data/lib/contrast/config/diagnostics/tools.rb +15 -5
- data/lib/contrast/config/yaml_file.rb +8 -0
- data/lib/contrast/configuration.rb +61 -29
- data/lib/contrast/framework/rails/support.rb +3 -0
- data/lib/contrast/logger/application.rb +3 -3
- data/lib/contrast/utils/assess/event_limit_utils.rb +13 -13
- data/lib/contrast/utils/assess/propagation_method_utils.rb +2 -0
- data/lib/contrast/utils/metrics_hash.rb +1 -1
- data/lib/contrast/utils/object_share.rb +2 -1
- data/lib/contrast/utils/os.rb +1 -9
- data/lib/contrast/utils/response_utils.rb +12 -0
- data/lib/contrast/utils/timer.rb +2 -0
- data/lib/contrast.rb +9 -2
- data/resources/assess/policy.json +80 -3
- data/ruby-agent.gemspec +1 -1
- metadata +22 -6
- data/lib/contrast/utils/input_classification_base.rb +0 -169
@@ -0,0 +1,191 @@
|
|
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/agent/protect/input_analyzer/input_analyzer'
|
6
|
+
require 'contrast/agent/protect/rule/input_classification/extendable'
|
7
|
+
require 'contrast/agent/protect/rule/input_classification/encoding'
|
8
|
+
require 'contrast/components/logger'
|
9
|
+
|
10
|
+
module Contrast
|
11
|
+
module Agent
|
12
|
+
module Protect
|
13
|
+
module Rule
|
14
|
+
module InputClassification
|
15
|
+
# This module will include all the similar information for all input classifications
|
16
|
+
# between different rules
|
17
|
+
module Base
|
18
|
+
UNKNOWN_KEY = 'unknown'
|
19
|
+
include Contrast::Components::Logger::InstanceMethods
|
20
|
+
include Contrast::Agent::Protect::Rule::InputClassification::Extendable
|
21
|
+
include Contrast::Agent::Protect::Rule::InputClassification::Encoding
|
22
|
+
|
23
|
+
KEYS_NEEDED = [
|
24
|
+
COOKIE_VALUE, PARAMETER_VALUE, HEADER, JSON_VALUE, MULTIPART_VALUE, XML_VALUE, DWR_VALUE
|
25
|
+
].cs__freeze
|
26
|
+
|
27
|
+
BASE64_INPUT_TYPES = [BODY, COOKIE_VALUE, HEADER, PARAMETER_VALUE, MULTIPART_VALUE, XML_VALUE].cs__freeze
|
28
|
+
|
29
|
+
class << self
|
30
|
+
include Contrast::Components::Logger::InstanceMethods
|
31
|
+
include Contrast::Agent::Reporting::InputType
|
32
|
+
|
33
|
+
# Finds key value and type based on input type and value.
|
34
|
+
# @param request [Contrast::Agent::Request] the current request context.
|
35
|
+
# @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
|
36
|
+
# @param value [String, Array<String>] the value of the input.
|
37
|
+
# @return [Array<(String, Contrast::Agent::Reporting::InputType)>] key and key type.
|
38
|
+
def find_key request, input_type, value
|
39
|
+
# TODO: RUBY-99999 Add handling for multipart, json and if any missing types.
|
40
|
+
case input_type
|
41
|
+
when COOKIE_VALUE
|
42
|
+
[request.cookies.key(value), Contrast::Agent::Reporting::InputType::COOKIE_NAME]
|
43
|
+
when PARAMETER_VALUE, URL_PARAMETER
|
44
|
+
[request.parameters.key(value), Contrast::Agent::Reporting::InputType::PARAMETER_NAME]
|
45
|
+
when HEADER
|
46
|
+
[request.headers.key(value), Contrast::Agent::Reporting::InputType::HEADER]
|
47
|
+
when UNKNOWN
|
48
|
+
[UNKNOWN_KEY, Contrast::Agent::Reporting::InputType::UNKNOWN]
|
49
|
+
else
|
50
|
+
[nil, nil]
|
51
|
+
end
|
52
|
+
rescue StandardError => e
|
53
|
+
logger.warn('[InputAnalyzer] Could not find proper key for input traced value', message: e)
|
54
|
+
[nil, nil]
|
55
|
+
end
|
56
|
+
|
57
|
+
# Some input types are not yet supported from the AgentLib.
|
58
|
+
# This will convert the type to the closet possible if viable,
|
59
|
+
# so that the input tracing could be done.
|
60
|
+
#
|
61
|
+
# @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
|
62
|
+
# @return [Integer<Contrast::AgentLib::Interface::INPUT_SET>]
|
63
|
+
def convert_input_type input_type
|
64
|
+
case input_type
|
65
|
+
when URI, URL_PARAMETER
|
66
|
+
Contrast::AGENT_LIB.input_set[:URI_PATH]
|
67
|
+
when BODY, DWR_VALUE, SOCKET, UNDEFINED_TYPE, UNKNOWN, REQUEST, QUERYSTRING
|
68
|
+
Contrast::AGENT_LIB.input_set[:PARAMETER_VALUE]
|
69
|
+
when HEADER
|
70
|
+
Contrast::AGENT_LIB.input_set[:HEADER_VALUE]
|
71
|
+
when MULTIPART_VALUE, MULTIPART_FIELD_NAME
|
72
|
+
Contrast::AGENT_LIB.input_set[:MULTIPART_NAME]
|
73
|
+
when JSON_ARRAYED_VALUE
|
74
|
+
Contrast::AGENT_LIB.input_set[:JSON_KEY]
|
75
|
+
when PARAMETER_NAME
|
76
|
+
Contrast::AGENT_LIB.input_set[:PARAMETER_KEY]
|
77
|
+
else
|
78
|
+
Contrast::AGENT_LIB.input_set[input_type]
|
79
|
+
end
|
80
|
+
rescue StandardError => e
|
81
|
+
logger.debug('[InputAnalyzer] Protect Input classification could not determine input type,
|
82
|
+
falling back to default',
|
83
|
+
error: e)
|
84
|
+
Contrast::AGENT_LIB.input_set[:PARAMETER_VALUE]
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Input Classification stage is done to determine if an user input is
|
89
|
+
# DEFINITEATTACK or to be ignored.
|
90
|
+
#
|
91
|
+
# @param rule_id [String] Name of the protect rule.
|
92
|
+
# @param input_type [Symbol, Contrast::Agent::Reporting::InputType] The type of the user input.
|
93
|
+
# @param value [String, Array<String>] the value of the input.
|
94
|
+
# @param input_analysis [Contrast::Agent::Reporting::InputAnalysis] Holds all the results from the
|
95
|
+
# agent analysis from the current
|
96
|
+
# Request.
|
97
|
+
# @return ia [Contrast::Agent::Reporting::InputAnalysis, nil] with updated results.
|
98
|
+
def classify rule_id, input_type, value, input_analysis
|
99
|
+
return unless (rule = Contrast::PROTECT.rule(rule_id))
|
100
|
+
return unless rule.applicable_user_inputs.include?(input_type)
|
101
|
+
return unless input_analysis.request
|
102
|
+
|
103
|
+
Array(value).each do |val|
|
104
|
+
Array(val).each do |v|
|
105
|
+
next unless v
|
106
|
+
|
107
|
+
result = create_new_input_result(input_analysis.request, rule.rule_name, input_type, v)
|
108
|
+
append_result(input_analysis, result)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
input_analysis
|
113
|
+
rescue StandardError => e
|
114
|
+
logger.debug("An Error was recorded in the input classification of the #{ rule_id }", error: e)
|
115
|
+
nil
|
116
|
+
end
|
117
|
+
|
118
|
+
# This methods checks if input is value that matches a key in the input.
|
119
|
+
#
|
120
|
+
# @param request [Contrast::Agent::Request] the current request context.
|
121
|
+
# @param ia_result [Contrast::Agent::Reporting::InputAnalysisResult] result to be updated.
|
122
|
+
# @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
|
123
|
+
# @param value [String, Array<String>] the value of the input.
|
124
|
+
#
|
125
|
+
# @return result [Array<String, Symbol>] updated with key result.
|
126
|
+
def add_needed_key request, ia_result, input_type, value
|
127
|
+
ia_result.key, ia_result.key_type = Contrast::Agent::Protect::Rule::InputClassification::Base.
|
128
|
+
find_key(request, input_type, value)
|
129
|
+
end
|
130
|
+
|
131
|
+
private
|
132
|
+
|
133
|
+
# Appends result to the InputAnalysis.
|
134
|
+
#
|
135
|
+
# @param ia_analysis [Contrast::Agent::Reporting::InputAnalysis] the current input analysis.
|
136
|
+
# @param result [Contrast::Agent::Reporting::InputAnalysisResult] result to be appended.
|
137
|
+
# @return [Contrast::Agent::Reporting::InputAnalysis] the input analysis with the appended result.
|
138
|
+
def append_result ia_analysis, result
|
139
|
+
ia_analysis.results << result if result
|
140
|
+
ia_analysis
|
141
|
+
end
|
142
|
+
|
143
|
+
# Do not override this method, it will hold base operations, instead overwrite methods called inside
|
144
|
+
# of this method.
|
145
|
+
# This methods checks if input is tagged WORTHWATCHING or IGNORE matches value with it's
|
146
|
+
# key if needed and Creates new instance of InputAnalysisResult.
|
147
|
+
#
|
148
|
+
# @param request [Contrast::Agent::Request] the current request context.
|
149
|
+
# @param rule_id [String] The name of the Protect Rule.
|
150
|
+
# @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
|
151
|
+
# @param value [String, Array<String>] the value of the input.
|
152
|
+
#
|
153
|
+
# @return res [Contrast::Agent::Reporting::InputAnalysisResult, nil]
|
154
|
+
def create_new_input_result request, rule_id, input_type, value
|
155
|
+
return unless Contrast::AGENT_LIB
|
156
|
+
|
157
|
+
# Cache retrieve
|
158
|
+
cached = Contrast::Agent::Protect::InputAnalyzer.lru_cache.lookout(rule_id, value, input_type, request)
|
159
|
+
return cached.result if cached.cs__is_a?(Contrast::Agent::Protect::InputClassification::CachedResult)
|
160
|
+
|
161
|
+
# Input evaluation
|
162
|
+
input_eval = build_input_eval(rule_id, input_type, base64_decode_input(value, input_type))
|
163
|
+
ia_result = build_ia_result(rule_id, input_type, value, request, input_eval)
|
164
|
+
return unless ia_result
|
165
|
+
|
166
|
+
add_needed_key(request, ia_result, input_type, value) if KEYS_NEEDED.include?(input_type)
|
167
|
+
# Input evaluation end
|
168
|
+
|
169
|
+
# Cache save. Cache must be saved after the input evaluation is completed.
|
170
|
+
Contrast::Agent::Protect::InputAnalyzer.lru_cache.save(rule_id, ia_result, request)
|
171
|
+
ia_result
|
172
|
+
end
|
173
|
+
|
174
|
+
# Decodes the value for the given input type.
|
175
|
+
# Applies to BODY, COOKIE_VALUE, HEADER, PARAMETER_VALUE, MULTIPART_VALUE, XML_VALUE
|
176
|
+
#
|
177
|
+
# @param value [String]
|
178
|
+
# @param input_type [Symbol]
|
179
|
+
# @return input [String]
|
180
|
+
def base64_decode_input value, input_type
|
181
|
+
return value unless Contrast::PROTECT.normalize_base64?
|
182
|
+
return value unless BASE64_INPUT_TYPES.include?(input_type)
|
183
|
+
|
184
|
+
cs__decode64(value, input_type)
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
@@ -0,0 +1,71 @@
|
|
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/agent/protect/rule/input_classification/encoding_rates'
|
5
|
+
require 'contrast/agent/telemetry/input_analysis_encoding_event'
|
6
|
+
require 'contrast/components/logger'
|
7
|
+
|
8
|
+
module Contrast
|
9
|
+
module Agent
|
10
|
+
module Protect
|
11
|
+
module Rule
|
12
|
+
module InputClassification
|
13
|
+
# This class will safe all the information for the Base64 decoding matches per input type.
|
14
|
+
class Base64Statistic
|
15
|
+
include Contrast::Components::Logger::InstanceMethods
|
16
|
+
|
17
|
+
# @return [Hash<Contrast::Agent::Protect::Rule::InputClassification::EncodingRates>]
|
18
|
+
attr_reader :data
|
19
|
+
|
20
|
+
# Capacity for request context life cycle.
|
21
|
+
CAPACITY = 1000
|
22
|
+
|
23
|
+
def initialize
|
24
|
+
@data = {}
|
25
|
+
end
|
26
|
+
|
27
|
+
# Add a match for the given input type.
|
28
|
+
#
|
29
|
+
# @param input_type [Symbol]
|
30
|
+
def match! input_type
|
31
|
+
return @data[input_type]&.increase_match_base64 if @data[input_type]
|
32
|
+
|
33
|
+
@data[input_type] = Contrast::Agent::Protect::Rule::InputClassification::EncodingRates.new(input_type)
|
34
|
+
@data[input_type].increase_match_base64
|
35
|
+
end
|
36
|
+
|
37
|
+
# Add a mismatch for the given input type.
|
38
|
+
#
|
39
|
+
# @param input_type [Symbol]
|
40
|
+
def mismatch! input_type
|
41
|
+
return @data[input_type]&.increase_mismatch_base64 if @data[input_type]
|
42
|
+
|
43
|
+
@data[input_type] = Contrast::Agent::Protect::Rule::InputClassification::EncodingRates.new(input_type)
|
44
|
+
@data[input_type].increase_mismatch_base64
|
45
|
+
end
|
46
|
+
|
47
|
+
# Clears statistic data.
|
48
|
+
def clear
|
49
|
+
@data.clear
|
50
|
+
end
|
51
|
+
|
52
|
+
# @return [Array<Contrast::Agent::Telemetry::InputAnalysisCacheEvent>] the events to be sent.
|
53
|
+
def to_events
|
54
|
+
events = []
|
55
|
+
data.each do |_input_type, encoding_rate|
|
56
|
+
event = Contrast::Agent::Telemetry::InputAnalysisEncodingEvent.new(nil, encoding_rate)
|
57
|
+
next if event.empty?
|
58
|
+
|
59
|
+
events << event
|
60
|
+
end
|
61
|
+
events
|
62
|
+
rescue StandardError => e
|
63
|
+
logger.error("[Telemetry] Error while creating events: #{ e }", stacktrace: e.backtrace)
|
64
|
+
[]
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,37 @@
|
|
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/duck_utils'
|
5
|
+
require 'contrast/agent/reporting/input_analysis/input_analysis_result'
|
6
|
+
|
7
|
+
module Contrast
|
8
|
+
module Agent
|
9
|
+
module Protect
|
10
|
+
module InputClassification
|
11
|
+
# This Class with store the input classification results for a given user input.
|
12
|
+
class CachedResult
|
13
|
+
# @return [String]
|
14
|
+
attr_reader :result
|
15
|
+
# @return [Integer]
|
16
|
+
attr_reader :request_id
|
17
|
+
|
18
|
+
# Initialize Input Classification Cached Result
|
19
|
+
#
|
20
|
+
# @param result [Contrast::Agent::Reporting::InputAnalysisResult]
|
21
|
+
# @param request_id [Integer] the id of current request.
|
22
|
+
def initialize result, request_id
|
23
|
+
@result = result.dup if result&.cs__is_a?(Contrast::Agent::Reporting::InputAnalysisResult)
|
24
|
+
@request_id = request_id
|
25
|
+
end
|
26
|
+
|
27
|
+
# Check if the input classification result is empty.
|
28
|
+
#
|
29
|
+
# @return [Boolean]
|
30
|
+
def empty?
|
31
|
+
Contrast::Utils::DuckUtils.empty_duck?(@result) && Contrast::Utils::DuckUtils.empty_duck?(@request_id)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,109 @@
|
|
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 'base64'
|
5
|
+
require 'cgi'
|
6
|
+
require 'uri'
|
7
|
+
|
8
|
+
module Contrast
|
9
|
+
module Agent
|
10
|
+
module Protect
|
11
|
+
module Rule
|
12
|
+
module InputClassification
|
13
|
+
# Module to hold different encoding utils.
|
14
|
+
module Encoding
|
15
|
+
include Contrast::Components::Logger::InstanceMethods
|
16
|
+
|
17
|
+
# Still a list is needed for this one, as it is not possible to determine if the value is encoded or not.
|
18
|
+
# As long as the list is short the method has a good percentage of success.
|
19
|
+
KNOWN_DECODING_EXCEPTIONS = %w[cmd].cs__freeze
|
20
|
+
|
21
|
+
# This methods is not performant, but is more safe for false positive.
|
22
|
+
# Base64 check is no trivial task. For example if one passes a value like 'stringdw' it will return true,
|
23
|
+
# or value 'pass', but it is indeed not encoded. using regexp like:
|
24
|
+
#
|
25
|
+
# ^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$
|
26
|
+
#
|
27
|
+
# This will fail with any inputs from above, and this is because any characters with 4 bytes will be
|
28
|
+
# considered as base64 encoded, and without additional context it is impossible to determine if the
|
29
|
+
# value is encoded or not. Not to mention the above regexp will not detect empty spaces.
|
30
|
+
#
|
31
|
+
# Alternative to the above regexp, acting the same way, could be this:
|
32
|
+
#
|
33
|
+
# Base64.strict_encode64(Base64.decode64(value)) == value
|
34
|
+
#
|
35
|
+
# Making an exception list is not a good idea, because it will be hard to maintain.
|
36
|
+
#
|
37
|
+
# The Base64 method will return printable ascii characters, so we can use this to determine if the value is
|
38
|
+
# encoded or not.
|
39
|
+
#
|
40
|
+
# The solution in this case is encodind the value, and then decoding it. If the value is already encoded
|
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
|
+
#
|
43
|
+
# @param value [String] input to check for encoding status.
|
44
|
+
# @param input_type [Symbol] input type.
|
45
|
+
# @return [Boolean] true if value is base64 encoded, false otherwise.
|
46
|
+
def cs__base64? value, input_type
|
47
|
+
return false unless value.is_a?(String)
|
48
|
+
return false if Contrast::Utils::DuckUtils.empty_duck?(value)
|
49
|
+
|
50
|
+
# Encoded string levels of decoding example:
|
51
|
+
#
|
52
|
+
# Value encoded 'pass' => 'cGFzcw=='
|
53
|
+
# decode level 0 => 'pass'
|
54
|
+
# decode level 1 => '\xA5\xAB,'
|
55
|
+
# decode level 2 => ''
|
56
|
+
check_value = value.dup
|
57
|
+
return false if KNOWN_DECODING_EXCEPTIONS.include?(check_value)
|
58
|
+
|
59
|
+
level = 0
|
60
|
+
iteration = 0
|
61
|
+
until Contrast::Utils::DuckUtils.empty_duck?(Base64.decode64(check_value))
|
62
|
+
iteration += 1
|
63
|
+
# handle cases like 'command' or 'injection' which will check out as encoded regarding the level of
|
64
|
+
# decoding, but will produce ascii escape characters on first iteration, rather than decoded value.
|
65
|
+
level += 1 unless iteration == 2 && ::CGI.escape(check_value) != check_value
|
66
|
+
|
67
|
+
check_value = Base64.decode64(check_value)
|
68
|
+
end
|
69
|
+
|
70
|
+
# if we have more than 2 levels the value is encoded.
|
71
|
+
base64 = level > 1
|
72
|
+
|
73
|
+
# Call base64 statistics:
|
74
|
+
if base64
|
75
|
+
Contrast::Agent::Protect::InputAnalyzer.base64_statistic.match!(input_type)
|
76
|
+
else
|
77
|
+
Contrast::Agent::Protect::InputAnalyzer.base64_statistic.mismatch!(input_type)
|
78
|
+
end
|
79
|
+
|
80
|
+
base64
|
81
|
+
rescue StandardError => e
|
82
|
+
logger.error('Error while checking for base64 encoding',
|
83
|
+
error: e,
|
84
|
+
message: e.message,
|
85
|
+
backtrace: e.backtrace)
|
86
|
+
false
|
87
|
+
end
|
88
|
+
|
89
|
+
# This method will decode the value using Base64.decode64, only if value was encoded.
|
90
|
+
# If value is not encoded, it will return the original value.
|
91
|
+
#
|
92
|
+
# @param value [String] input to decode.
|
93
|
+
# @param input_type [Symbol] input type.
|
94
|
+
# @return [String] decoded or original value.
|
95
|
+
# @raise [StandardError]
|
96
|
+
def cs__decode64 value, input_type
|
97
|
+
return value unless cs__base64?(value, input_type)
|
98
|
+
|
99
|
+
Base64.decode64(value)
|
100
|
+
rescue StandardError => e
|
101
|
+
logger.error('Error while decoding base64', error: e, message: e.message, backtrace: e.backtrace)
|
102
|
+
value
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,47 @@
|
|
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/agent/protect/rule/input_classification/rates'
|
5
|
+
|
6
|
+
module Contrast
|
7
|
+
module Agent
|
8
|
+
module Protect
|
9
|
+
module Rule
|
10
|
+
module InputClassification
|
11
|
+
# This class will hold match information when input classification is being saved in LRU cache.
|
12
|
+
class EncodingRates < Contrast::Agent::Protect::Rule::InputClassification::Rates
|
13
|
+
# @return [Integer]
|
14
|
+
attr_reader :base64_matches
|
15
|
+
# @return [Integer]
|
16
|
+
attr_reader :base64_mismatches
|
17
|
+
|
18
|
+
def initialize input_type
|
19
|
+
super(nil, input_type)
|
20
|
+
@base64_matches = 0
|
21
|
+
@base64_mismatches = 0
|
22
|
+
end
|
23
|
+
|
24
|
+
# Increase the match count for the given rule_id and input.
|
25
|
+
def increase_match_base64
|
26
|
+
@base64_matches += 1
|
27
|
+
end
|
28
|
+
|
29
|
+
def increase_mismatch_base64
|
30
|
+
@base64_mismatches += 1
|
31
|
+
end
|
32
|
+
|
33
|
+
# Returns Agent Telemetry reportable fields.
|
34
|
+
#
|
35
|
+
# @return [Hash]
|
36
|
+
def to_fields
|
37
|
+
{
|
38
|
+
"#{ to_field_title }.input_base64_matches" => base64_matches,
|
39
|
+
"#{ to_field_title }.input_base64_mismatches" => base64_mismatches
|
40
|
+
}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,80 @@
|
|
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/agent/reporting/input_analysis/input_type'
|
5
|
+
require 'contrast/agent/reporting/input_analysis/score_level'
|
6
|
+
|
7
|
+
module Contrast
|
8
|
+
module Agent
|
9
|
+
module Protect
|
10
|
+
module Rule
|
11
|
+
module InputClassification
|
12
|
+
# Module holding the overwritable methods for input classification. This is used by the
|
13
|
+
# Protect rules to define their own input classification logic. To be Used input_types,
|
14
|
+
# score_level, AgentLib, and InputAnalysisResult must be required.
|
15
|
+
module Extendable
|
16
|
+
THRESHOLD = 90.cs__freeze
|
17
|
+
WORTHWATCHING_THRESHOLD = 10.cs__freeze
|
18
|
+
include Contrast::Agent::Reporting::InputType
|
19
|
+
include Contrast::Agent::Reporting::ScoreLevel
|
20
|
+
|
21
|
+
################################################################
|
22
|
+
# Methods to be overwritten for each individual Protect rule. #
|
23
|
+
##############################################################
|
24
|
+
|
25
|
+
# Creates new instance of AgentLib evaluation result with direct call to AgentLib.
|
26
|
+
#
|
27
|
+
# @param rule_id [String] The name of the Protect Rule.
|
28
|
+
# @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
|
29
|
+
# @param value [String] the value of the input.
|
30
|
+
# @return [Contrast::AgentLib::EvalResult, nil] the result of the input evaluation.
|
31
|
+
def build_input_eval rule_id, input_type, value
|
32
|
+
Contrast::AGENT_LIB.eval_input(value,
|
33
|
+
Contrast::Agent::Protect::Rule::InputClassification::Base.
|
34
|
+
convert_input_type(input_type),
|
35
|
+
Contrast::AGENT_LIB.rule_set[rule_id],
|
36
|
+
Contrast::AGENT_LIB.eval_option[:PREFER_WORTH_WATCHING])
|
37
|
+
end
|
38
|
+
|
39
|
+
# Creates specific result from the AgentLib evaluation.
|
40
|
+
#
|
41
|
+
# @param rule_id [String] The name of the Protect Rule.
|
42
|
+
# @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
|
43
|
+
# @param value [String the value of the input.
|
44
|
+
# @param request [Contrast::Agent::Request] the current request context.
|
45
|
+
# @param input_eval [Contrast::AgentLib::EvalResult] the result of the input evaluation.
|
46
|
+
# @return [Contrast::Agent::Reporting::InputAnalysisResult, nil] the result of the input analysis.
|
47
|
+
def build_ia_result rule_id, input_type, value, request, input_eval
|
48
|
+
ia_result = new_ia_result(rule_id, input_type, request.path, value)
|
49
|
+
score = input_eval&.score || 0
|
50
|
+
if score >= WORTHWATCHING_THRESHOLD
|
51
|
+
ia_result.score_level = WORTHWATCHING
|
52
|
+
ia_result.ids << self::WORTHWATCHING_MATCH
|
53
|
+
else
|
54
|
+
ia_result.score_level = IGNORE
|
55
|
+
end
|
56
|
+
ia_result
|
57
|
+
end
|
58
|
+
|
59
|
+
# Creates new isntance of InputAnalysisResult with basic info.
|
60
|
+
#
|
61
|
+
# @param rule_id [String] The name of the Protect Rule.
|
62
|
+
# @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
|
63
|
+
# @param value [String, Array<String>] the value of the input.
|
64
|
+
# @param path [String] the path of the current request context.
|
65
|
+
#
|
66
|
+
# @return res [Contrast::Agent::Reporting::InputAnalysisResult]
|
67
|
+
def new_ia_result rule_id, input_type, path, value = nil
|
68
|
+
res = Contrast::Agent::Reporting::InputAnalysisResult.new
|
69
|
+
res.rule_id = rule_id
|
70
|
+
res.input_type = input_type
|
71
|
+
res.path = path
|
72
|
+
res.value = value
|
73
|
+
res
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|