contrast-agent 5.1.0 → 5.2.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/ext/cs__assess_kernel/cs__assess_kernel.c +7 -4
- data/ext/cs__assess_module/cs__assess_module.c +7 -7
- data/ext/cs__common/cs__common.c +4 -0
- data/ext/cs__common/cs__common.h +1 -0
- data/ext/cs__contrast_patch/cs__contrast_patch.c +52 -27
- data/ext/cs__contrast_patch/cs__contrast_patch.h +2 -0
- data/ext/cs__scope/cs__scope.c +747 -0
- data/ext/cs__scope/cs__scope.h +88 -0
- data/ext/cs__scope/extconf.rb +5 -0
- data/lib/contrast/agent/assess/contrast_event.rb +20 -13
- data/lib/contrast/agent/assess/contrast_object.rb +4 -1
- data/lib/contrast/agent/assess/policy/propagation_node.rb +2 -5
- data/lib/contrast/agent/assess/policy/propagator/match_data.rb +2 -0
- data/lib/contrast/agent/assess/policy/trigger_method.rb +4 -1
- data/lib/contrast/agent/assess/rule/response/{autocomplete_rule.rb → auto_complete_rule.rb} +4 -3
- data/lib/contrast/agent/assess/rule/response/base_rule.rb +12 -79
- data/lib/contrast/agent/assess/rule/response/body_rule.rb +109 -0
- data/lib/contrast/agent/assess/rule/response/cache_control_header_rule.rb +157 -0
- data/lib/contrast/agent/assess/rule/response/click_jacking_header_rule.rb +26 -0
- data/lib/contrast/agent/assess/rule/response/csp_header_insecure_rule.rb +14 -15
- data/lib/contrast/agent/assess/rule/response/csp_header_missing_rule.rb +5 -25
- data/lib/contrast/agent/assess/rule/response/framework/rails_support.rb +29 -0
- data/lib/contrast/agent/assess/rule/response/header_rule.rb +70 -0
- data/lib/contrast/agent/assess/rule/response/hsts_header_rule.rb +12 -36
- data/lib/contrast/agent/assess/rule/response/parameters_pollution_rule.rb +2 -1
- data/lib/contrast/agent/assess/rule/response/x_content_type_header_rule.rb +26 -0
- data/lib/contrast/agent/assess/rule/response/x_xss_protection_header_rule.rb +36 -0
- data/lib/contrast/agent/middleware.rb +1 -0
- data/lib/contrast/agent/patching/policy/after_load_patcher.rb +1 -3
- data/lib/contrast/agent/patching/policy/patch.rb +2 -6
- data/lib/contrast/agent/patching/policy/patcher.rb +1 -1
- data/lib/contrast/agent/protect/input_analyzer/input_analyzer.rb +94 -0
- data/lib/contrast/agent/protect/rule/base.rb +28 -1
- data/lib/contrast/agent/protect/rule/base_service.rb +10 -1
- data/lib/contrast/agent/protect/rule/cmd_injection.rb +2 -0
- data/lib/contrast/agent/protect/rule/deserialization.rb +6 -0
- data/lib/contrast/agent/protect/rule/http_method_tampering.rb +5 -1
- data/lib/contrast/agent/protect/rule/no_sqli.rb +1 -0
- data/lib/contrast/agent/protect/rule/path_traversal.rb +1 -0
- data/lib/contrast/agent/protect/rule/sqli/sqli_input_classification.rb +124 -0
- data/lib/contrast/agent/protect/rule/sqli/sqli_worth_watching.rb +121 -0
- data/lib/contrast/agent/protect/rule/sqli.rb +33 -0
- data/lib/contrast/agent/protect/rule/xxe.rb +4 -0
- data/lib/contrast/agent/reporting/input_analysis/input_analysis.rb +44 -0
- data/lib/contrast/agent/reporting/input_analysis/input_analysis_result.rb +115 -0
- data/lib/contrast/agent/reporting/input_analysis/input_type.rb +44 -0
- data/lib/contrast/agent/reporting/input_analysis/score_level.rb +21 -0
- data/lib/contrast/agent/reporting/report.rb +1 -0
- data/lib/contrast/agent/reporting/reporter.rb +8 -1
- data/lib/contrast/agent/reporting/reporting_events/finding.rb +69 -36
- data/lib/contrast/agent/reporting/reporting_events/finding_event.rb +88 -59
- data/lib/contrast/agent/reporting/reporting_events/{finding_object.rb → finding_event_object.rb} +24 -20
- data/lib/contrast/agent/reporting/reporting_events/finding_event_parent_object.rb +39 -0
- data/lib/contrast/agent/reporting/reporting_events/finding_event_property.rb +40 -0
- data/lib/contrast/agent/reporting/reporting_events/{finding_signature.rb → finding_event_signature.rb} +29 -24
- data/lib/contrast/agent/reporting/reporting_events/finding_event_source.rb +12 -8
- data/lib/contrast/agent/reporting/reporting_events/{finding_stack.rb → finding_event_stack.rb} +23 -19
- data/lib/contrast/agent/reporting/reporting_events/{finding_taint_range.rb → finding_event_taint_range.rb} +17 -15
- data/lib/contrast/agent/reporting/reporting_events/finding_request.rb +26 -53
- data/lib/contrast/agent/reporting/reporting_events/poll.rb +29 -0
- data/lib/contrast/agent/reporting/reporting_events/reporting_event.rb +5 -4
- data/lib/contrast/agent/reporting/reporting_events/route_discovery.rb +1 -0
- data/lib/contrast/agent/reporting/reporting_events/server_activity.rb +1 -1
- data/lib/contrast/agent/reporting/reporting_utilities/audit.rb +10 -3
- data/lib/contrast/agent/reporting/reporting_utilities/endpoints.rb +0 -1
- data/lib/contrast/agent/reporting/reporting_utilities/reporter_client.rb +1 -0
- data/lib/contrast/agent/reporting/reporting_utilities/reporter_client_utils.rb +28 -20
- data/lib/contrast/agent/reporting/reporting_utilities/response_handler.rb +1 -1
- data/lib/contrast/agent/reporting/reporting_utilities/response_handler_utils.rb +13 -1
- data/lib/contrast/agent/request_context.rb +6 -1
- data/lib/contrast/agent/request_context_extend.rb +85 -21
- data/lib/contrast/agent/scope.rb +102 -107
- data/lib/contrast/agent/service_heartbeat.rb +45 -2
- data/lib/contrast/agent/version.rb +1 -1
- data/lib/contrast/api/decorators/bot_blocker.rb +37 -0
- data/lib/contrast/api/decorators/ip_denylist.rb +37 -0
- data/lib/contrast/api/decorators/rasp_rule_sample.rb +29 -0
- data/lib/contrast/api/decorators/user_input.rb +11 -1
- data/lib/contrast/api/decorators/virtual_patch.rb +34 -0
- data/lib/contrast/components/logger.rb +5 -0
- data/lib/contrast/components/protect.rb +4 -2
- data/lib/contrast/components/scope.rb +98 -91
- data/lib/contrast/config/agent_configuration.rb +58 -12
- data/lib/contrast/config/api_configuration.rb +100 -12
- data/lib/contrast/config/api_proxy_configuration.rb +55 -3
- data/lib/contrast/config/application_configuration.rb +114 -15
- data/lib/contrast/config/assess_configuration.rb +106 -12
- data/lib/contrast/config/assess_rules_configuration.rb +44 -3
- data/lib/contrast/config/base_configuration.rb +1 -0
- data/lib/contrast/config/certification_configuration.rb +74 -3
- data/lib/contrast/config/exception_configuration.rb +61 -3
- data/lib/contrast/config/heap_dump_configuration.rb +101 -17
- data/lib/contrast/config/inventory_configuration.rb +64 -3
- data/lib/contrast/config/logger_configuration.rb +46 -3
- data/lib/contrast/config/protect_rule_configuration.rb +36 -9
- data/lib/contrast/config/protect_rules_configuration.rb +120 -17
- data/lib/contrast/config/request_audit_configuration.rb +68 -3
- data/lib/contrast/config/ruby_configuration.rb +96 -22
- data/lib/contrast/config/sampling_configuration.rb +76 -10
- data/lib/contrast/config/server_configuration.rb +56 -11
- data/lib/contrast/configuration.rb +6 -3
- data/lib/contrast/logger/cef_log.rb +151 -0
- data/lib/contrast/utils/hash_digest.rb +14 -6
- data/lib/contrast/utils/log_utils.rb +114 -0
- data/lib/contrast/utils/middleware_utils.rb +6 -7
- data/lib/contrast/utils/net_http_base.rb +12 -9
- data/lib/contrast/utils/patching/policy/patch_utils.rb +0 -4
- data/lib/contrast.rb +4 -3
- data/ruby-agent.gemspec +1 -1
- data/service_executables/VERSION +1 -1
- data/service_executables/linux/contrast-service +0 -0
- data/service_executables/mac/contrast-service +0 -0
- metadata +41 -21
- data/lib/contrast/agent/assess/rule/response/cachecontrol_rule.rb +0 -184
- data/lib/contrast/agent/assess/rule/response/clickjacking_rule.rb +0 -66
- data/lib/contrast/agent/assess/rule/response/x_content_type_rule.rb +0 -52
- data/lib/contrast/agent/assess/rule/response/x_xss_protection_rule.rb +0 -53
- data/lib/contrast/extension/kernel.rb +0 -54
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require 'contrast/agent/protect/rule/base'
|
|
5
|
+
require 'contrast/components/logger'
|
|
5
6
|
|
|
6
7
|
module Contrast
|
|
7
8
|
module Agent
|
|
@@ -11,6 +12,8 @@ module Contrast
|
|
|
11
12
|
# Deserialization Protect rule.
|
|
12
13
|
class Deserialization < Contrast::Agent::Protect::Rule::Base
|
|
13
14
|
# The TeamServer recognized name of this rule
|
|
15
|
+
include Contrast::Components::Logger::InstanceMethods
|
|
16
|
+
|
|
14
17
|
NAME = 'untrusted-deserialization'
|
|
15
18
|
|
|
16
19
|
# The rule specific reason for raising a security exception.
|
|
@@ -78,6 +81,8 @@ module Contrast
|
|
|
78
81
|
result = build_attack_with_match(context, ia_result, nil, serialized_input, **kwargs)
|
|
79
82
|
append_to_activity(context, result)
|
|
80
83
|
|
|
84
|
+
cef_logging result, :successful_attack
|
|
85
|
+
|
|
81
86
|
raise Contrast::SecurityException.new(self, block_message) if blocked?
|
|
82
87
|
end
|
|
83
88
|
|
|
@@ -97,6 +102,7 @@ module Contrast
|
|
|
97
102
|
ia_result = build_evaluation(gadget_command)
|
|
98
103
|
result = build_attack_with_match(context, ia_result, nil, gadget_command, **kwargs)
|
|
99
104
|
append_to_activity(context, result)
|
|
105
|
+
cef_logging result, :successful_attack, gadget_command
|
|
100
106
|
raise Contrast::SecurityException.new(self, BLOCK_MESSAGE) if blocked?
|
|
101
107
|
end
|
|
102
108
|
|
|
@@ -34,7 +34,11 @@ module Contrast
|
|
|
34
34
|
else
|
|
35
35
|
build_attack_with_match(context, nil, nil, nil, method: method, response_code: response_code)
|
|
36
36
|
end
|
|
37
|
-
|
|
37
|
+
|
|
38
|
+
return unless result
|
|
39
|
+
|
|
40
|
+
append_to_activity(context, result)
|
|
41
|
+
cef_logging result, :ineffective_attack
|
|
38
42
|
end
|
|
39
43
|
|
|
40
44
|
protected
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Copyright (c) 2022 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/reporting/input_analysis/input_type'
|
|
6
|
+
require 'contrast/agent/protect/rule/sqli'
|
|
7
|
+
require 'contrast/agent/reporting/input_analysis/score_level'
|
|
8
|
+
require 'contrast/agent/protect/rule/sqli/sqli_worth_watching'
|
|
9
|
+
require 'contrast/agent/protect/input_analyzer/input_analyzer'
|
|
10
|
+
|
|
11
|
+
module Contrast
|
|
12
|
+
module Agent
|
|
13
|
+
module Protect
|
|
14
|
+
module Rule
|
|
15
|
+
# This module will do the Input Classification stage of SQLI rule
|
|
16
|
+
# as a result input would be marked as WORTHWATCHING or IGNORE,
|
|
17
|
+
# to be analyzed at the sink level.
|
|
18
|
+
module SqliInputClassification
|
|
19
|
+
class << self
|
|
20
|
+
include Contrast::Agent::Reporting::InputType
|
|
21
|
+
include Contrast::Agent::Reporting::ScoreLevel
|
|
22
|
+
include Contrast::Agent::Protect::Rule::SqliWorthWatching
|
|
23
|
+
|
|
24
|
+
WORTHWATCHING_MATCH = 'sqli-worth-watching-v2'
|
|
25
|
+
SQLI_KEYS_NEEDED = [
|
|
26
|
+
COOKIE_VALUE, PARAMETER_VALUE, JSON_VALUE, MULTIPART_VALUE, XML_VALUE, DWR_VALUE
|
|
27
|
+
].cs__freeze
|
|
28
|
+
|
|
29
|
+
# Input Classification stage is done to determine if an user input is
|
|
30
|
+
# WORTHWATCHING or to be ignored.
|
|
31
|
+
#
|
|
32
|
+
# @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
|
|
33
|
+
# @param value [String, Array<String>] the value of the input.
|
|
34
|
+
# @param input_analysis [Contrast::Agent::Reporting::InputAnalysis] Holds all the results from the
|
|
35
|
+
# agent analysis from the current
|
|
36
|
+
# Request.
|
|
37
|
+
# @return ia [Contrast::Agent::Reporting::InputAnalysis] with updated results.
|
|
38
|
+
def classify input_type, value, input_analysis
|
|
39
|
+
return unless Contrast::Agent::Protect::Rule::Sqli::APPLICABLE_USER_INPUTS.include?(input_type)
|
|
40
|
+
return unless input_analysis.request
|
|
41
|
+
|
|
42
|
+
rule_id = Contrast::Agent::Protect::Rule::Sqli::NAME
|
|
43
|
+
results = []
|
|
44
|
+
|
|
45
|
+
# double check the input to avoid calling match? on array
|
|
46
|
+
Array(value).each do |val|
|
|
47
|
+
Array(val).each do |v|
|
|
48
|
+
results << sqli_create_new_input_result(input_analysis.request, rule_id, input_type, v)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
input_analysis.results = results
|
|
53
|
+
input_analysis
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
# Creates new isntance of InputAnalysisResult with basic info.
|
|
59
|
+
#
|
|
60
|
+
# @param rule_id [String] The name of the Protect Rule.
|
|
61
|
+
# @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
|
|
62
|
+
# @param score_level [Contrast::Agent::Reporting::ScoreLevel] the score tag after analysis.
|
|
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, score_level, path, value
|
|
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.score_level = score_level
|
|
73
|
+
res.value = value
|
|
74
|
+
res
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# This methods checks if input is tagged WORTHWATCHING or IGNORE matches value with it's
|
|
78
|
+
# key if needed and Creates new isntance of InputAnalysisResult.
|
|
79
|
+
#
|
|
80
|
+
# @param request [Contrast::Agent::Request] the current request context.
|
|
81
|
+
# @param rule_id [String] The name of the Protect Rule.
|
|
82
|
+
# @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
|
|
83
|
+
# @param value [String, Array<String>] the value of the input.
|
|
84
|
+
#
|
|
85
|
+
# @return res [Contrast::Agent::Reporting::InputAnalysisResult]
|
|
86
|
+
def sqli_create_new_input_result request, rule_id, input_type, value
|
|
87
|
+
result = if sqli_worth_watching? value
|
|
88
|
+
ia_result = new_ia_result(rule_id, input_type, WORTHWATCHING, request.path, value)
|
|
89
|
+
ia_result.ids << WORTHWATCHING_MATCH
|
|
90
|
+
ia_result
|
|
91
|
+
else
|
|
92
|
+
new_ia_result(rule_id, input_type, IGNORE, request.path, value)
|
|
93
|
+
end
|
|
94
|
+
if SQLI_KEYS_NEEDED.include? input_type
|
|
95
|
+
result.key = sqli_add_needed_key request, result, input_type, value
|
|
96
|
+
end
|
|
97
|
+
result
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# This methods checks if input is value that matches a key in the input.
|
|
101
|
+
#
|
|
102
|
+
# @param request [Contrast::Agent::Request] the current request context.
|
|
103
|
+
# @param result [Contrast::Agent::Reporting::InputAnalysisResult] result to be updated.
|
|
104
|
+
# @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
|
|
105
|
+
# @param value [String, Array<String>] the value of the input.
|
|
106
|
+
#
|
|
107
|
+
# @return result [Contrast::Agent::Reporting::InputAnalysisResult] updated with key result.
|
|
108
|
+
def sqli_add_needed_key request, result, input_type, value
|
|
109
|
+
case input_type
|
|
110
|
+
when COOKIE_VALUE
|
|
111
|
+
result.key = request.cookies.key(value)
|
|
112
|
+
when PARAMETER_VALUE
|
|
113
|
+
result.key = request.parameters.key(value)
|
|
114
|
+
else
|
|
115
|
+
result.key
|
|
116
|
+
end
|
|
117
|
+
result.key
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Copyright (c) 2022 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
|
+
|
|
6
|
+
module Contrast
|
|
7
|
+
module Agent
|
|
8
|
+
module Protect
|
|
9
|
+
module Rule
|
|
10
|
+
# This module implements the sqli-worth-watching-v2 check to determine whether input
|
|
11
|
+
# is IGNORED or WORTH_WATCHING. If WORTH_WATCHING => analyze at sink level.
|
|
12
|
+
# https://protect-spec.prod.dotnet.contsec.com/rules/sql-injection.html#input-tracing
|
|
13
|
+
module SqliWorthWatching
|
|
14
|
+
COLOR_CODE = /^#[0-9A-Fa-f]{6}$/.cs__freeze
|
|
15
|
+
ALPHA_NUMERIC_AND_SPACES = /^+[a-zA-Z\d\s]+$/.cs__freeze
|
|
16
|
+
OR_CLAUSE = /[oO][rR]/.cs__freeze
|
|
17
|
+
EXPLOITABLE_SUBSTRING = %w[# -- // /*].cs__freeze
|
|
18
|
+
SUSPICIOUS_CHARS = %w[` " \' ; - % , ( ) | { } =].cs__freeze
|
|
19
|
+
SQL_COMMENTS = %w[# /* */ // -- @@].cs__freeze
|
|
20
|
+
BLOCK_START = '/*'.cs__freeze
|
|
21
|
+
BLOCK_END = '*/'.cs__freeze
|
|
22
|
+
SQL_KEYWORDS = %w[
|
|
23
|
+
alter begin between create case column_name
|
|
24
|
+
current_user delete drop exec execute from
|
|
25
|
+
group insert limit like merge order outfile
|
|
26
|
+
select session_user syslogins update union
|
|
27
|
+
UTL_INADDR UTL_HTTP
|
|
28
|
+
].cs__freeze
|
|
29
|
+
|
|
30
|
+
# This method will determine if a user input is Worth watching and return true if it is.
|
|
31
|
+
# This is done by running checks, and if the inputs is worth to watch it would be
|
|
32
|
+
# saved for the later sink sqli input analysis.
|
|
33
|
+
#
|
|
34
|
+
# @param input [String] the user input to be inspected
|
|
35
|
+
# @return true | false
|
|
36
|
+
def sqli_worth_watching? input
|
|
37
|
+
return false if input.nil? || input.empty?
|
|
38
|
+
|
|
39
|
+
exploitable?(input) && (
|
|
40
|
+
input.match?(OR_CLAUSE) || sql_comments?(input) || suspicious_chars?(input) || language_keywords?(input)
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
# Check if input is exploitable, with min length set to 3 chars
|
|
47
|
+
#
|
|
48
|
+
# @param input [String] the user input to be inspected
|
|
49
|
+
# @return true | false
|
|
50
|
+
def exploitable? input
|
|
51
|
+
return false if input.length < 3 && input.match?(ALPHA_NUMERIC_AND_SPACES)
|
|
52
|
+
return false if input.length == 3 && !contains_substring?(input, EXPLOITABLE_SUBSTRING)
|
|
53
|
+
return false if input.length == 7 && input.match?(COLOR_CODE)
|
|
54
|
+
|
|
55
|
+
true
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Check if input contains sqli comments:
|
|
59
|
+
# '# /* */ // -- @@'
|
|
60
|
+
#
|
|
61
|
+
# @param input [String] the user input to be inspected
|
|
62
|
+
# @return true | false
|
|
63
|
+
def sql_comments? input
|
|
64
|
+
input.length >= 3 && contains_substring?(input, SQL_COMMENTS)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Check if input contains block comments starting and
|
|
68
|
+
# ending with '/*..*/'
|
|
69
|
+
#
|
|
70
|
+
# @param input [String] the user input to be inspected
|
|
71
|
+
# @return true | false
|
|
72
|
+
def block_comments? input
|
|
73
|
+
idx1 = input.index(BLOCK_START)
|
|
74
|
+
idx2 = input.index(BLOCK_END)
|
|
75
|
+
|
|
76
|
+
return false if idx1.nil? || idx2.nil?
|
|
77
|
+
|
|
78
|
+
(idx1 >= 0 && idx2 >= 2 && (idx1 < idx2))
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Runs the input against suspicious chars array.
|
|
82
|
+
#
|
|
83
|
+
# @param input [String] the user input to be inspected
|
|
84
|
+
# @return true | false
|
|
85
|
+
def suspicious_chars? input
|
|
86
|
+
input.length >= 7 && (number_of_substrings(input, SUSPICIOUS_CHARS) >= 2 || block_comments?(input))
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Runs the input against SQL language preserved words.
|
|
90
|
+
#
|
|
91
|
+
# @param input [String] the user input to be inspected
|
|
92
|
+
# @return true | false
|
|
93
|
+
def language_keywords? input
|
|
94
|
+
contains_substring? input, SQL_KEYWORDS
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Helper method to find a substrings in given input.
|
|
98
|
+
#
|
|
99
|
+
# @param substrings [Array] set of substrings to inspect.
|
|
100
|
+
# @return true | false
|
|
101
|
+
def contains_substring? input, substrings
|
|
102
|
+
return true if substrings.any? { |sub| input.include?(sub) }
|
|
103
|
+
|
|
104
|
+
false
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Helper method to find the number of substrings.
|
|
108
|
+
#
|
|
109
|
+
# @param input [String] the user input to be inspected
|
|
110
|
+
# @return number [Integer] Number of substrings
|
|
111
|
+
def number_of_substrings input, substring
|
|
112
|
+
number = 0
|
|
113
|
+
input.each_char.reduce(0) { |_acc, elem| number += 1 if contains_substring?(elem, substring) }
|
|
114
|
+
|
|
115
|
+
number
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
require 'contrast/agent/protect/rule/base_service'
|
|
5
5
|
require 'contrast/agent/protect/policy/applies_sqli_rule'
|
|
6
6
|
require 'contrast/agent/protect/rule/sql_sample_builder'
|
|
7
|
+
require 'contrast/agent/reporting/input_analysis/input_type'
|
|
7
8
|
|
|
8
9
|
module Contrast
|
|
9
10
|
module Agent
|
|
@@ -16,7 +17,17 @@ module Contrast
|
|
|
16
17
|
include SqlSampleBuilder::SqliSample
|
|
17
18
|
# Defining build_attack_with_match method
|
|
18
19
|
include SqlSampleBuilder::AttackBuilder
|
|
20
|
+
include Contrast::Agent::Reporting::InputType
|
|
21
|
+
class << self
|
|
22
|
+
include Contrast::Agent::Reporting::InputType
|
|
23
|
+
end
|
|
19
24
|
|
|
25
|
+
APPLICABLE_USER_INPUTS = [
|
|
26
|
+
BODY, COOKIE_NAME, COOKIE_VALUE, HEADER,
|
|
27
|
+
PARAMETER_NAME, PARAMETER_VALUE, JSON_VALUE,
|
|
28
|
+
MULTIPART_VALUE, MULTIPART_FIELD_NAME,
|
|
29
|
+
XML_VALUE, DWR_VALUE
|
|
30
|
+
].cs__freeze
|
|
20
31
|
NAME = 'sql-injection'
|
|
21
32
|
BLOCK_MESSAGE = 'SQLi rule triggered. Response blocked.'
|
|
22
33
|
|
|
@@ -36,8 +47,30 @@ module Contrast
|
|
|
36
47
|
|
|
37
48
|
append_to_activity(context, result)
|
|
38
49
|
|
|
50
|
+
cef_logging result, :successful_attack, query_string
|
|
39
51
|
raise Contrast::SecurityException.new(self, BLOCK_MESSAGE) if blocked?
|
|
40
52
|
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def find_attacker context, potential_attack_string, **kwargs
|
|
57
|
+
ia_results = gather_ia_results(context)
|
|
58
|
+
find_attacker_with_results(context, potential_attack_string, ia_results, **kwargs)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def infilter? context
|
|
62
|
+
return false unless context&.agent_input_analysis&.results
|
|
63
|
+
return false unless enabled?
|
|
64
|
+
return false if protect_excluded_by_code?
|
|
65
|
+
|
|
66
|
+
true
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def gather_ia_results context
|
|
70
|
+
context.agent_input_analysis.results.select do |ia_result|
|
|
71
|
+
ia_result.rule_id == rule_name
|
|
72
|
+
end
|
|
73
|
+
end
|
|
41
74
|
end
|
|
42
75
|
end
|
|
43
76
|
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
require 'contrast/agent/protect/rule/base'
|
|
5
5
|
require 'contrast/utils/timer'
|
|
6
|
+
require 'contrast/components/logger'
|
|
6
7
|
|
|
7
8
|
module Contrast
|
|
8
9
|
module Agent
|
|
@@ -11,6 +12,8 @@ module Contrast
|
|
|
11
12
|
# Implementation of the XXE Protect Rule used to evaluate XML calls for exploit
|
|
12
13
|
# of unsafe external entity resolution.
|
|
13
14
|
class Xxe < Contrast::Agent::Protect::Rule::Base
|
|
15
|
+
include Contrast::Components::Logger::InstanceMethods
|
|
16
|
+
|
|
14
17
|
NAME = 'xxe'
|
|
15
18
|
BLOCK_MESSAGE = 'XXE rule triggered. Response blocked.'
|
|
16
19
|
EXTERNAL_ENTITY_PATTERN = /<!ENTITY\s+[a-zA-Z0-f]+\s+(?:SYSTEM|PUBLIC)\s+(.*?)>/.cs__freeze
|
|
@@ -36,6 +39,7 @@ module Contrast
|
|
|
36
39
|
append_to_activity(context, result)
|
|
37
40
|
return unless blocked?
|
|
38
41
|
|
|
42
|
+
cef_logging result, :successful_attack, xml
|
|
39
43
|
raise Contrast::SecurityException.new(self, BLOCK_MESSAGE)
|
|
40
44
|
end
|
|
41
45
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Copyright (c) 2022 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/reporting/input_analysis/input_analysis_result'
|
|
6
|
+
|
|
7
|
+
module Contrast
|
|
8
|
+
module Agent
|
|
9
|
+
module Reporting
|
|
10
|
+
# This class will do ia analysis for our protect rules instead of
|
|
11
|
+
# using the service.
|
|
12
|
+
class InputAnalysis
|
|
13
|
+
# result from input analysis
|
|
14
|
+
#
|
|
15
|
+
# @return @_results [Array<Contrast::Agent::Reporting::Settings::InputAnalysisResult>]
|
|
16
|
+
def results
|
|
17
|
+
@_results ||= []
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# result from input analysis
|
|
21
|
+
#
|
|
22
|
+
# @return @_results [Array<Contrast::Agent::Reporting::Settings::InputAnalysisResult>]
|
|
23
|
+
def results= results
|
|
24
|
+
@_results = results
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Returns our wrapper around the Rack::Request for this context
|
|
28
|
+
#
|
|
29
|
+
# @return request [Contrast::Agent::Request, nil]
|
|
30
|
+
def request
|
|
31
|
+
@_request ||= nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Sets current request
|
|
35
|
+
#
|
|
36
|
+
# @param request [Contrast::Agent::Request] our wrapper around the Rack::Request for this context
|
|
37
|
+
# @return request [Contrast::Agent::Request, nil]
|
|
38
|
+
def request= request
|
|
39
|
+
@_request = request if request.instance_of?(Contrast::Agent::Request)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Copyright (c) 2022 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/reporting/input_analysis/input_type'
|
|
6
|
+
require 'contrast/agent/reporting/input_analysis/score_level'
|
|
7
|
+
|
|
8
|
+
module Contrast
|
|
9
|
+
module Agent
|
|
10
|
+
module Reporting
|
|
11
|
+
# This class will do ia analysis for our protect rules instead of
|
|
12
|
+
# using the service.
|
|
13
|
+
class InputAnalysisResult
|
|
14
|
+
INPUT_TYPE = Contrast::Agent::Reporting::InputType
|
|
15
|
+
SCORE_LEVEL = Contrast::Agent::Reporting::ScoreLevel
|
|
16
|
+
|
|
17
|
+
# @return @_rule_id [String]
|
|
18
|
+
def rule_id
|
|
19
|
+
@_rule_id ||= Contrast::Utils::ObjectShare::EMPTY_STRING
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @param id [String]
|
|
23
|
+
# @return @_rule_id [String]
|
|
24
|
+
def rule_id= id
|
|
25
|
+
@_rule_id = id if id.is_a?(String)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @return @_input_type [
|
|
29
|
+
# Symbol<Contrast::Agent::Reporting::Settings::InputAnalysis::InputAnalysisResult::InputType>]
|
|
30
|
+
def input_type
|
|
31
|
+
@_input_type ||= INPUT_TYPE::UNDEFINED_TYPE
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @param input_type [
|
|
35
|
+
# Symbol<Contrast::Agent::Reporting::Settings::InputAnalysis::InputAnalysisResult::InputType>]
|
|
36
|
+
# @return @_input_type [
|
|
37
|
+
# Symbol<Contrast::Agent::Reporting::Settings::InputAnalysis::InputAnalysisResult::InputType>]
|
|
38
|
+
def input_type= input_type
|
|
39
|
+
@_input_type = input_type if INPUT_TYPE.to_a.include?(input_type)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @return @_path [String]
|
|
43
|
+
def path
|
|
44
|
+
@_path ||= Contrast::Utils::ObjectShare::EMPTY_STRING
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @param path [String]
|
|
48
|
+
# @return @_path [String]
|
|
49
|
+
def path= path
|
|
50
|
+
@_path = path if path.is_a?(String)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @return @_key [String]
|
|
54
|
+
def key
|
|
55
|
+
@_key ||= Contrast::Utils::ObjectShare::EMPTY_STRING
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @param key [String]
|
|
59
|
+
# @return @_key [String]
|
|
60
|
+
def key= key
|
|
61
|
+
@_key = key if key.is_a?(String)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @return value [String]
|
|
65
|
+
def value
|
|
66
|
+
@_value ||= Contrast::Utils::ObjectShare::EMPTY_STRING
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# @param value [String]
|
|
70
|
+
# @return value [String]
|
|
71
|
+
def value= value
|
|
72
|
+
@_value = value if value.is_a?(String)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Matchers IDs
|
|
76
|
+
# @return @_ids [Array<String>]
|
|
77
|
+
def ids
|
|
78
|
+
@_ids ||= []
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Matchers IDs
|
|
82
|
+
# @param ids [Array<String>]
|
|
83
|
+
# @return @_ids [Array<String>]
|
|
84
|
+
def ids= ids
|
|
85
|
+
@_ids = ids if ids.is_a?(Array) && ids.any?(String)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# @return @_attack_count [Integer]
|
|
89
|
+
def attack_count
|
|
90
|
+
@_attack_count ||= 0
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# @param attack_count
|
|
94
|
+
# @return @_attack_count
|
|
95
|
+
def attack_count= attack_count
|
|
96
|
+
@_attack_count = attack_count if attack_count.is_a?(Integer)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# @return @_score_level [
|
|
100
|
+
# Symbol<Contrast::Agent::Reporting::Settings::InputAnalysis::InputAnalysisResult::ScoreLevel>]
|
|
101
|
+
def score_level
|
|
102
|
+
@_score_level ||= SCORE_LEVEL::IGNORE
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# @param score_level [
|
|
106
|
+
# Symbol<Contrast::Agent::Reporting::Settings::InputAnalysis::InputAnalysisResult::ScoreLevel>]
|
|
107
|
+
# @return @_score_level [
|
|
108
|
+
# Symbol<Contrast::Agent::Reporting::Settings::InputAnalysis::InputAnalysisResult::ScoreLevel>]
|
|
109
|
+
def score_level= score_level
|
|
110
|
+
@_score_level = score_level if SCORE_LEVEL.to_a.include?(score_level)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Contrast
|
|
5
|
+
module Agent
|
|
6
|
+
module Reporting
|
|
7
|
+
# input types for InputAnalysis results
|
|
8
|
+
module InputType
|
|
9
|
+
UNDEFINED_TYPE = :UNDEFINED_TYPE.cs__freeze
|
|
10
|
+
BODY = :BODY.cs__freeze
|
|
11
|
+
COOKIE_NAME = :COOKIE_NAME.cs__freeze
|
|
12
|
+
COOKIE_VALUE = :COOKIE_VALUE.cs__freeze
|
|
13
|
+
HEADER = :HEADER.cs__freeze
|
|
14
|
+
PARAMETER_NAME = :PARAMETER_NAME.cs__freeze
|
|
15
|
+
PARAMETER_VALUE = :PARAMETER_VALUE.cs__freeze
|
|
16
|
+
QUERYSTRING = :QUERYSTRING.cs__freeze
|
|
17
|
+
URI = :URI.cs__freeze
|
|
18
|
+
SOCKET = :SOCKET.cs__freeze
|
|
19
|
+
JSON_VALUE = :JSON_VALUE.cs__freeze
|
|
20
|
+
JSON_ARRAYED_VALUE = :JSON_ARRAYED_VALUE.cs__freeze
|
|
21
|
+
MULTIPART_CONTENT_TYPE = :MULTIPART_CONTENT_TYPE.cs__freeze
|
|
22
|
+
MULTIPART_VALUE = :MULTIPART_VALUE.cs__freeze
|
|
23
|
+
MULTIPART_FIELD_NAME = :MULTIPART_FIELD_NAME.cs__freeze
|
|
24
|
+
MULTIPART_NAME = :MULTIPART_NAME.cs__freeze
|
|
25
|
+
XML_VALUE = :XML_VALUE.cs__freeze
|
|
26
|
+
DWR_VALUE = :DWR_VALUE.cs__freeze
|
|
27
|
+
METHOD = :METHOD.cs__freeze
|
|
28
|
+
REQUEST = :REQUEST.cs__freeze
|
|
29
|
+
URL_PARAMETER = :URL_PARAMETER.cs__freeze
|
|
30
|
+
UNKNOWN = :UNKNOWN.cs__freeze
|
|
31
|
+
|
|
32
|
+
class << self
|
|
33
|
+
def to_a
|
|
34
|
+
[
|
|
35
|
+
UNDEFINED_TYPE, BODY, COOKIE_NAME, COOKIE_VALUE, HEADER, PARAMETER_NAME, PARAMETER_VALUE,
|
|
36
|
+
QUERYSTRING, URI, SOCKET, JSON_VALUE, JSON_ARRAYED_VALUE, MULTIPART_CONTENT_TYPE, MULTIPART_VALUE,
|
|
37
|
+
MULTIPART_FIELD_NAME, MULTIPART_NAME, XML_VALUE, DWR_VALUE, METHOD, REQUEST, URL_PARAMETER, UNKNOWN
|
|
38
|
+
]
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Contrast
|
|
5
|
+
module Agent
|
|
6
|
+
module Reporting
|
|
7
|
+
# input types for InputAnalysis results
|
|
8
|
+
module ScoreLevel
|
|
9
|
+
IGNORE = :DONTCARE.cs__freeze
|
|
10
|
+
WORTHWATCHING = :WORTHWATCHING.cs__freeze
|
|
11
|
+
DEFINITEATTACK = :DEFINITEATTACK.cs__freeze
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
def to_a
|
|
15
|
+
[IGNORE, WORTHWATCHING, DEFINITEATTACK]
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -25,3 +25,4 @@ require 'contrast/agent/reporting/reporting_events/preflight'
|
|
|
25
25
|
require 'contrast/agent/reporting/reporting_events/observed_route'
|
|
26
26
|
require 'contrast/agent/reporting/reporting_events/route_coverage'
|
|
27
27
|
require 'contrast/agent/reporting/reporting_events/observed_library_usage'
|
|
28
|
+
require 'contrast/agent/reporting/reporting_events/poll'
|
|
@@ -53,6 +53,11 @@ module Contrast
|
|
|
53
53
|
@_thread = Contrast::Agent::Thread.new do
|
|
54
54
|
logger.debug('Starting background Reporter thread.')
|
|
55
55
|
loop do
|
|
56
|
+
# TODO: RUBY-99999
|
|
57
|
+
# The client and connection are being used in multiple threads/ concurrently, and that's not okay. We need
|
|
58
|
+
# to figure out why that is and lock it so that it isn't.
|
|
59
|
+
next unless client && connection
|
|
60
|
+
|
|
56
61
|
event = queue.pop
|
|
57
62
|
begin
|
|
58
63
|
response = client.send_event(event, connection)
|
|
@@ -86,7 +91,9 @@ module Contrast
|
|
|
86
91
|
return
|
|
87
92
|
end
|
|
88
93
|
response = client.send_event(event, connection, true)
|
|
89
|
-
|
|
94
|
+
return unless response
|
|
95
|
+
|
|
96
|
+
client.handle_response(event, response, connection)
|
|
90
97
|
audit&.audit_event(event, response) if ::Contrast::API.request_audit_enable?
|
|
91
98
|
rescue StandardError => e
|
|
92
99
|
logger.error('Could not send message to service from Reporter queue.', e)
|