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,7 +2,7 @@
|
|
|
2
2
|
# Copyright (c) 2021 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
|
|
3
3
|
# frozen_string_literal: true
|
|
4
4
|
|
|
5
|
-
require 'contrast/agent/assess/rule/response/
|
|
5
|
+
require 'contrast/agent/assess/rule/response/header_rule'
|
|
6
6
|
require 'contrast/utils/string_utils'
|
|
7
7
|
|
|
8
8
|
module Contrast
|
|
@@ -11,27 +11,22 @@ module Contrast
|
|
|
11
11
|
module Rule
|
|
12
12
|
module Response
|
|
13
13
|
# These rules check that the HTTP Headers include CSP header types
|
|
14
|
-
class CspHeaderInsecure <
|
|
14
|
+
class CspHeaderInsecure < HeaderRule
|
|
15
15
|
def rule_id
|
|
16
16
|
'csp-header-insecure'
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
protected
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
HEADER_KEYS = %w[Content-Security-Policy X-Content-Security-Policy X-Webkit-CSP].cs__freeze
|
|
22
|
+
DEFAULT_SAFE = false
|
|
22
23
|
SETTINGS = %w[
|
|
23
24
|
base-uri child-src default-src connect-src frame-src media-src object-src script-src
|
|
24
25
|
style-src form-action frame-ancestors plugin-types reflected-xss referer
|
|
25
26
|
].cs__freeze
|
|
26
27
|
UNSAFE_VALUE_REGEXP = /^unsafe-(?:inline|eval)$/.cs__freeze
|
|
27
28
|
ASTERISK_REGEXP = /[*]/.cs__freeze
|
|
28
|
-
|
|
29
|
-
# Rules discern which responses they can/should analyze.
|
|
30
|
-
#
|
|
31
|
-
# @param response [Contrast::Agent::Response] the response of the application
|
|
32
|
-
def analyze_response? response
|
|
33
|
-
super && headers?(response)
|
|
34
|
-
end
|
|
29
|
+
SAFE_REFLECTED_XSS = /1/.cs__freeze
|
|
35
30
|
|
|
36
31
|
# Determine if the Response violates the Rule or not. If it does, return the evidence that proves it so.
|
|
37
32
|
#
|
|
@@ -39,16 +34,19 @@ module Contrast
|
|
|
39
34
|
# @return [Contrast::Utils::ObjectShare::EMPTY_STRING, nil] if CSP Header is not found
|
|
40
35
|
def violated? response
|
|
41
36
|
settings = {}
|
|
42
|
-
csp_hash =
|
|
37
|
+
csp_hash = get_header_value(response)
|
|
43
38
|
return if csp_hash.nil?
|
|
44
39
|
|
|
45
40
|
SETTINGS.each do |setting_attr|
|
|
41
|
+
# default src has to be checked all other keys may be missing
|
|
42
|
+
next unless csp_hash.key?(setting_attr) || setting_attr == 'default-src'
|
|
43
|
+
|
|
46
44
|
value = csp_hash[setting_attr]
|
|
47
45
|
key = convert_key(setting_attr)
|
|
48
46
|
settings["#{ key }Secure"] = !value.nil? && value_secure?(value) && value_safe?(value)
|
|
49
47
|
settings["#{ key }Value"] = value.nil? ? Contrast::Utils::ObjectShare::EMPTY_STRING : value
|
|
50
48
|
end
|
|
51
|
-
|
|
49
|
+
evidence(settings) if settings.value?(false)
|
|
52
50
|
end
|
|
53
51
|
|
|
54
52
|
# Get the CSP values from and transforms them to key value hash
|
|
@@ -58,9 +56,10 @@ module Contrast
|
|
|
58
56
|
#
|
|
59
57
|
# @param headers [Hash] the response of the application
|
|
60
58
|
# @return [Array, nil] array of CSP header values
|
|
61
|
-
def
|
|
59
|
+
def get_header_value response
|
|
62
60
|
csp_hash = {}
|
|
63
|
-
|
|
61
|
+
headers = response.headers
|
|
62
|
+
HEADER_KEYS.each do |header_key|
|
|
64
63
|
next unless headers[header_key]&.length&.positive?
|
|
65
64
|
|
|
66
65
|
values = headers[header_key].split(Contrast::Utils::ObjectShare::SEMICOLON)
|
|
@@ -78,7 +77,7 @@ module Contrast
|
|
|
78
77
|
end
|
|
79
78
|
|
|
80
79
|
def value_safe? value
|
|
81
|
-
UNSAFE_VALUE_REGEXP.match(value).nil?
|
|
80
|
+
UNSAFE_VALUE_REGEXP.match(value).nil? || !SAFE_REFLECTED_XSS.match(value).nil?
|
|
82
81
|
end
|
|
83
82
|
|
|
84
83
|
# Converts the CSP key to camelcase to be used as key for evidence object
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
require 'contrast/agent/assess/rule/response/
|
|
4
|
+
require 'contrast/agent/assess/rule/response/header_rule'
|
|
5
5
|
require 'contrast/utils/string_utils'
|
|
6
6
|
|
|
7
7
|
module Contrast
|
|
@@ -10,34 +10,14 @@ module Contrast
|
|
|
10
10
|
module Rule
|
|
11
11
|
module Response
|
|
12
12
|
# These rules check that the HTTP Headers include CSP header types
|
|
13
|
-
class CspHeaderMissing <
|
|
13
|
+
class CspHeaderMissing < HeaderRule
|
|
14
14
|
def rule_id
|
|
15
15
|
'csp-header-missing'
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
DATA = 'data'.cs__freeze
|
|
23
|
-
|
|
24
|
-
# Rules discern which responses they can/should analyze.
|
|
25
|
-
#
|
|
26
|
-
# @param response [Contrast::Agent::Response] the response of the application
|
|
27
|
-
def analyze_response? response
|
|
28
|
-
super && headers?(response)
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
# Determine if the Response violates the Rule or not. If it does, return the evidence that proves it so.
|
|
32
|
-
#
|
|
33
|
-
# @param response [Contrast::Agent::Response] the response of the application
|
|
34
|
-
# @return [Contrast::Utils::ObjectShare::EMPTY_STRING, nil] if CSP Header is not found
|
|
35
|
-
def violated? response
|
|
36
|
-
response_headers = response.headers
|
|
37
|
-
return if CSP_HEADERS.any? { |header_key| response_headers[header_key]&.length&.positive? }
|
|
38
|
-
|
|
39
|
-
{ DATA => Contrast::Utils::ObjectShare::EMPTY_STRING }
|
|
40
|
-
end
|
|
18
|
+
HEADER_KEYS = %w[Content-Security-Policy X-Content-Security-Policy X-Webkit-CSP].cs__freeze
|
|
19
|
+
ACCEPTED_VALUES = [/(.)/].cs__freeze
|
|
20
|
+
DEFAULT_SAFE = false
|
|
41
21
|
end
|
|
42
22
|
end
|
|
43
23
|
end
|
|
@@ -0,0 +1,29 @@
|
|
|
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 'rails'
|
|
5
|
+
|
|
6
|
+
module Contrast
|
|
7
|
+
module Agent
|
|
8
|
+
module Assess
|
|
9
|
+
module Rule
|
|
10
|
+
module Response
|
|
11
|
+
module Framework
|
|
12
|
+
# Rails 7 supports managing potential unsafe Headers
|
|
13
|
+
# this module contains methods for checking if Rails 7 supercedes our rules
|
|
14
|
+
module RailsSupport
|
|
15
|
+
RAILS_VERSION = Gem::Version.new('7.0.0')
|
|
16
|
+
|
|
17
|
+
def framework_supported?
|
|
18
|
+
rails_version = ::Rails.version
|
|
19
|
+
return false unless !!rails_version
|
|
20
|
+
|
|
21
|
+
Gem::Version.new(rails_version) >= RAILS_VERSION
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
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 'rack'
|
|
5
|
+
require 'contrast/agent/reporting/reporting_utilities/dtm_message'
|
|
6
|
+
require 'contrast/utils/hash_digest'
|
|
7
|
+
require 'contrast/utils/preflight_util'
|
|
8
|
+
require 'contrast/utils/string_utils'
|
|
9
|
+
require 'contrast/agent/assess/rule/response/base_rule'
|
|
10
|
+
|
|
11
|
+
module Contrast
|
|
12
|
+
module Agent
|
|
13
|
+
module Assess
|
|
14
|
+
module Rule
|
|
15
|
+
module Response
|
|
16
|
+
# These rules check the content of the HTTP Response to determine if something was set incorrectly or
|
|
17
|
+
# insecurely in it.
|
|
18
|
+
class HeaderRule < BaseRule
|
|
19
|
+
HEADER_TYPE = 'header'
|
|
20
|
+
|
|
21
|
+
# Rules discern which responses they can/should analyze.
|
|
22
|
+
#
|
|
23
|
+
# @param response [Contrast::Agent::Response] the response of the application
|
|
24
|
+
def analyze_response? response
|
|
25
|
+
super && headers?(response)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Determine if the Response violates the Rule or not. If it does, return the evidence that proves it so.
|
|
29
|
+
#
|
|
30
|
+
# @param response [Contrast::Agent::Response] the response of the application
|
|
31
|
+
# @return [Hash, nil] the evidence required to prove the violation of the rule
|
|
32
|
+
def violated? response
|
|
33
|
+
header_value = get_header_value(response)
|
|
34
|
+
if header_value
|
|
35
|
+
return evidence(header_value) unless valid_header?(header_value)
|
|
36
|
+
else
|
|
37
|
+
return evidence(header_value) unless cs__class::DEFAULT_SAFE
|
|
38
|
+
end
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Determine if a response has headers.
|
|
43
|
+
#
|
|
44
|
+
# @param response [Contrast::Agent::Response] the response of the application
|
|
45
|
+
# @return [Boolean]
|
|
46
|
+
def headers? response
|
|
47
|
+
response.headers&.any?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
protected
|
|
51
|
+
|
|
52
|
+
def get_header_value response
|
|
53
|
+
response_headers = response.headers
|
|
54
|
+
values = response_headers.values_at(*cs__class::HEADER_KEYS, *cs__class::HEADER_KEYS.map(&:to_sym))
|
|
55
|
+
values.compact.first
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Determine if the value of the Response Header has a valid value
|
|
59
|
+
#
|
|
60
|
+
# @param header [Contrast::Agent::Response] a response header
|
|
61
|
+
# @return [Boolean] whether the header value is valid
|
|
62
|
+
def valid_header? header
|
|
63
|
+
cs__class::ACCEPTED_VALUES.any? { |val| val.match(header) }
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
require 'contrast/agent/assess/rule/response/header_rule'
|
|
5
|
+
require 'contrast/utils/string_utils'
|
|
6
|
+
|
|
4
7
|
module Contrast
|
|
5
8
|
module Agent
|
|
6
9
|
module Assess
|
|
@@ -8,49 +11,22 @@ module Contrast
|
|
|
8
11
|
module Response
|
|
9
12
|
# This rule checks if the HTTP Headers include HSTS header and ensures that the max-age value
|
|
10
13
|
# is set to a value greater than 0.
|
|
11
|
-
class HSTSHeader <
|
|
14
|
+
class HSTSHeader < HeaderRule
|
|
12
15
|
def rule_id
|
|
13
16
|
'hsts-header-missing'
|
|
14
17
|
end
|
|
15
18
|
|
|
16
19
|
protected
|
|
17
20
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
MAX_AGE_SYM = MAX_AGE.to_sym
|
|
22
|
-
# Rules discern which responses they can/should analyze.
|
|
23
|
-
#
|
|
24
|
-
# @param response [Contrast::Agent::Response] the response of the application
|
|
25
|
-
def analyze_response? response
|
|
26
|
-
super && response.headers.cs__is_a?(Hash)
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
# Determine if the Response violates the Rule or not. If it does, return the evidence that proves it so.
|
|
30
|
-
#
|
|
31
|
-
# @param response [Contrast::Agent::Response] the response of the application
|
|
32
|
-
# @return [Hash<data: Contrast::Utils::ObjectShare::EMPTY_STRING, String>, nil] return string
|
|
33
|
-
# representation of the max_age
|
|
34
|
-
def violated? response
|
|
35
|
-
headers = response.headers
|
|
36
|
-
target = headers[HEADER_KEY] || headers[HEADER_KEY_SYM]
|
|
37
|
-
# this rule is safe by default if no target => no evidence
|
|
38
|
-
# if the property max_age is not positive or absent then the rule is violated
|
|
39
|
-
return unless target
|
|
40
|
-
|
|
41
|
-
max_age = target[MAX_AGE] || target[MAX_AGE_SYM]
|
|
42
|
-
return if max_age.to_i.positive?
|
|
43
|
-
|
|
44
|
-
evidence max_age
|
|
45
|
-
end
|
|
21
|
+
HEADER_KEYS = %w[Strict-Transport-Security].cs__freeze
|
|
22
|
+
ACCEPTED_VALUES = [/max-age=(\.)?\d+(\.\d*)?/].cs__freeze
|
|
23
|
+
DEFAULT_SAFE = true
|
|
46
24
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def evidence max_age
|
|
53
|
-
{ data: max_age.to_s }
|
|
25
|
+
def evidence data
|
|
26
|
+
# get only the value of the max-age property
|
|
27
|
+
val = data&.split('=')&.last
|
|
28
|
+
val = Contrast::Utils::ObjectShare::EMPTY_STRING if val.nil? || val == 'max-age'
|
|
29
|
+
{ DATA => val }
|
|
54
30
|
end
|
|
55
31
|
end
|
|
56
32
|
end
|
|
@@ -12,6 +12,7 @@ module Contrast
|
|
|
12
12
|
# These rules check the content of the HTTP Response to determine if the body contains a form which
|
|
13
13
|
# incorrectly sets the action attribute.
|
|
14
14
|
class ParametersPollution < BaseRule
|
|
15
|
+
include BodyRule
|
|
15
16
|
def rule_id
|
|
16
17
|
'parameter-pollution'
|
|
17
18
|
end
|
|
@@ -31,7 +32,7 @@ module Contrast
|
|
|
31
32
|
# @return [Hash, nil] the evidence required to prove the violation of the rule
|
|
32
33
|
def violated? response
|
|
33
34
|
body = response.body
|
|
34
|
-
forms =
|
|
35
|
+
forms = html_elements(body, FORM_START_REGEXP, true)
|
|
35
36
|
forms.each do |form|
|
|
36
37
|
# Because TeamServer will reject any subsequent form on the same page due to deduplication, we can
|
|
37
38
|
# skip out on the first violation.
|
|
@@ -0,0 +1,26 @@
|
|
|
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/agent/assess/rule/response/header_rule'
|
|
5
|
+
require 'contrast/utils/string_utils'
|
|
6
|
+
|
|
7
|
+
module Contrast
|
|
8
|
+
module Agent
|
|
9
|
+
module Assess
|
|
10
|
+
module Rule
|
|
11
|
+
module Response
|
|
12
|
+
# These rules check the content of the HTTP Response to determine if the response contains the needed header
|
|
13
|
+
class XContentType < HeaderRule
|
|
14
|
+
def rule_id
|
|
15
|
+
'xcontenttype-header-missing'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
HEADER_KEYS = %w[X-Content-Type-Options].cs__freeze
|
|
19
|
+
ACCEPTED_VALUES = [/^nosniff/i].cs__freeze
|
|
20
|
+
DEFAULT_SAFE = false
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
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/agent/assess/rule/response/header_rule'
|
|
5
|
+
require 'contrast/utils/string_utils'
|
|
6
|
+
require 'contrast/agent/assess/rule/response/framework/rails_support'
|
|
7
|
+
require 'rails'
|
|
8
|
+
|
|
9
|
+
module Contrast
|
|
10
|
+
module Agent
|
|
11
|
+
module Assess
|
|
12
|
+
module Rule
|
|
13
|
+
module Response
|
|
14
|
+
# These rules check the content of the HTTP Response to determine if the response contains the needed header
|
|
15
|
+
class XXssProtection < HeaderRule
|
|
16
|
+
include Framework::RailsSupport
|
|
17
|
+
|
|
18
|
+
def rule_id
|
|
19
|
+
'xxssprotection-header-disabled'
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
HEADER_KEYS = %w[X-XSS-Protection].cs__freeze
|
|
23
|
+
ACCEPTED_VALUES = [/^1/].cs__freeze
|
|
24
|
+
DEFAULT_SAFE = true
|
|
25
|
+
|
|
26
|
+
protected
|
|
27
|
+
|
|
28
|
+
def analyze_response? response
|
|
29
|
+
!framework_supported? && super
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -29,14 +29,13 @@ module Contrast
|
|
|
29
29
|
# there are no require time side effects of loading our core
|
|
30
30
|
# extensions.
|
|
31
31
|
def apply_direct_patches!
|
|
32
|
-
@_apply_direct_patches ||= begin
|
|
32
|
+
@_apply_direct_patches ||= begin # TODO: RUBY-1541 - put 'kernel' back
|
|
33
33
|
paths = %w[
|
|
34
34
|
array
|
|
35
35
|
basic_object
|
|
36
36
|
module
|
|
37
37
|
fiber_track
|
|
38
38
|
hash
|
|
39
|
-
kernel
|
|
40
39
|
marshal_module
|
|
41
40
|
regexp
|
|
42
41
|
string
|
|
@@ -75,7 +74,6 @@ module Contrast
|
|
|
75
74
|
def apply_require_patches!
|
|
76
75
|
@_apply_require_patches ||= begin
|
|
77
76
|
require 'contrast/extension/thread'
|
|
78
|
-
require 'contrast/extension/kernel'
|
|
79
77
|
true
|
|
80
78
|
rescue LoadError, StandardError => e
|
|
81
79
|
logger.error('failed instrumenting apply_require_patches!', e)
|
|
@@ -49,15 +49,11 @@ module Contrast
|
|
|
49
49
|
].cs__freeze
|
|
50
50
|
|
|
51
51
|
def enter_method_scope! method_policy
|
|
52
|
-
method_policy.scopes_to_enter
|
|
53
|
-
enter_scope!(scope)
|
|
54
|
-
end
|
|
52
|
+
contrast_enter_method_scopes! method_policy.scopes_to_enter
|
|
55
53
|
end
|
|
56
54
|
|
|
57
55
|
def exit_method_scope! method_policy
|
|
58
|
-
method_policy.scopes_to_exit
|
|
59
|
-
exit_scope!(scope)
|
|
60
|
-
end
|
|
56
|
+
contrast_exit_method_scopes! method_policy.scopes_to_exit
|
|
61
57
|
end
|
|
62
58
|
|
|
63
59
|
# @param mod [Module] the module in which the patch should be
|
|
@@ -147,7 +147,7 @@ module Contrast
|
|
|
147
147
|
return
|
|
148
148
|
end
|
|
149
149
|
|
|
150
|
-
patch_methods
|
|
150
|
+
patch_methods(status, module_data, module_policy)
|
|
151
151
|
rescue StandardError => e
|
|
152
152
|
status&.failed_patch!
|
|
153
153
|
logger.warn('Patching failed', e, module: module_data.mod_name)
|
|
@@ -0,0 +1,94 @@
|
|
|
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/agent/reporting/input_analysis/input_type'
|
|
5
|
+
require 'contrast/agent/reporting/input_analysis/score_level'
|
|
6
|
+
require 'contrast/agent/reporting/input_analysis/input_analysis'
|
|
7
|
+
require 'contrast/agent/protect/rule/sqli/sqli_input_classification'
|
|
8
|
+
require 'json'
|
|
9
|
+
|
|
10
|
+
module Contrast
|
|
11
|
+
module Agent
|
|
12
|
+
module Protect
|
|
13
|
+
# InputAnalyzer will extract input form current request context and will analyze it.
|
|
14
|
+
# This will be used in for the SQLI and CMDI worth_watching_v2 implementations.
|
|
15
|
+
module InputAnalyzer
|
|
16
|
+
class << self
|
|
17
|
+
include Contrast::Agent::Reporting::InputType
|
|
18
|
+
include Contrast::Agent::Reporting::ScoreLevel
|
|
19
|
+
include Contrast::Agent::Protect::Rule::SqliWorthWatching
|
|
20
|
+
|
|
21
|
+
PROTECT_RULES = { sqli: 'sql-injection' }.cs__freeze
|
|
22
|
+
|
|
23
|
+
# This method with analyze the user input from the context of the
|
|
24
|
+
# current request and run each of the protect rules against all
|
|
25
|
+
# found input types
|
|
26
|
+
#
|
|
27
|
+
# @param request [Contrast::Agent::Request] current request context.
|
|
28
|
+
# @return input_analysis [Contrast::Agent::Reporting::InputAnalysis, nil]
|
|
29
|
+
def analyse request
|
|
30
|
+
return unless Contrast::SETTINGS.protect_state.enabled
|
|
31
|
+
return if request.nil?
|
|
32
|
+
|
|
33
|
+
inputs = extract_input request
|
|
34
|
+
return unless inputs
|
|
35
|
+
|
|
36
|
+
input_analysis = Contrast::Agent::Reporting::InputAnalysis.new
|
|
37
|
+
input_analysis.request = request
|
|
38
|
+
# each rule against each input
|
|
39
|
+
input_classification inputs, input_analysis
|
|
40
|
+
input_analysis
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
# classify input by rule implementation of worth_watching_v2 for the rules supporting it.
|
|
46
|
+
#
|
|
47
|
+
# @param inputs [String, Array<String>] user input to be analysed.
|
|
48
|
+
# @param input_analysis [Contrast::Agent::Reporting::InputAnalysis] Here we will keep all the results
|
|
49
|
+
# for each protect rule.
|
|
50
|
+
# @return input_analysis [Contrast::Agent::Reporting::InputAnalysis, nil]
|
|
51
|
+
def input_classification inputs, input_analysis
|
|
52
|
+
# key = input type, value = user_input
|
|
53
|
+
inputs.each do |input_type, value|
|
|
54
|
+
PROTECT_RULES.each do |_key, rule_id|
|
|
55
|
+
# check if rule is enabled
|
|
56
|
+
next unless Contrast::PROTECT.rule(rule_id).enabled?
|
|
57
|
+
|
|
58
|
+
# start with sqli rule
|
|
59
|
+
case rule_id
|
|
60
|
+
when PROTECT_RULES[:sqli]
|
|
61
|
+
Contrast::Agent::Protect::Rule::SqliInputClassification.classify input_type, value, input_analysis
|
|
62
|
+
else
|
|
63
|
+
return nil
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
input_analysis
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Extract the inputs from the request context and label them with Protect
|
|
71
|
+
# input type tags. Each tag will contain one or more user inputs.
|
|
72
|
+
#
|
|
73
|
+
# This methods is to be expanded and modified as needed by other Protect rules
|
|
74
|
+
# and sub-rules for their requirements.
|
|
75
|
+
#
|
|
76
|
+
# @param request [Contrast::Agent::Request] current request context.
|
|
77
|
+
# @return inputs [Hash<Contrast::Agent::Protect::InputType => user_inputs>]
|
|
78
|
+
def extract_input request
|
|
79
|
+
inputs = {}
|
|
80
|
+
inputs[BODY] = request.body
|
|
81
|
+
inputs[COOKIE_NAME] = request.cookies.keys
|
|
82
|
+
inputs[COOKIE_VALUE] = request.cookies.values
|
|
83
|
+
inputs[HEADER] = request.headers
|
|
84
|
+
inputs[PARAMETER_NAME] = request.parameters.keys
|
|
85
|
+
inputs[PARAMETER_VALUE] = request.parameters.values
|
|
86
|
+
inputs[QUERYSTRING] = request.query_string
|
|
87
|
+
inputs.compact!
|
|
88
|
+
inputs
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -173,6 +173,19 @@ module Contrast
|
|
|
173
173
|
context.activity.results << result if result
|
|
174
174
|
end
|
|
175
175
|
|
|
176
|
+
# With this we log to CEF
|
|
177
|
+
#
|
|
178
|
+
# @param result [Contrast::Api::Dtm::AttackResult]
|
|
179
|
+
# @param attack [Symbol] the type of message we want to send
|
|
180
|
+
# @param value [String] the input value we want to log
|
|
181
|
+
def cef_logging result, attack = :ineffective_attack, value = nil
|
|
182
|
+
sample_to_json = Contrast::Api::Dtm::RaspRuleSample.to_controlled_hash result.samples[0]
|
|
183
|
+
outcome = Contrast::Api::Dtm::AttackResult::ResponseType.get_name_by_tag(result.response)
|
|
184
|
+
input_type = extract_input_type sample_to_json[:user_input].input_type
|
|
185
|
+
input_value = value || sample_to_json[:user_input].value
|
|
186
|
+
cef_logger.send(attack, result.rule_id, outcome, input_type, input_value)
|
|
187
|
+
end
|
|
188
|
+
|
|
176
189
|
protected
|
|
177
190
|
|
|
178
191
|
def mode_from_settings
|
|
@@ -232,7 +245,13 @@ module Contrast
|
|
|
232
245
|
|
|
233
246
|
def update_perimeter_attack_response context, ia_result, result
|
|
234
247
|
if mode == Contrast::Api::Settings::ProtectionRule::Mode::BLOCK_AT_PERIMETER
|
|
235
|
-
result.response = Contrast::
|
|
248
|
+
result.response = if ia_result&.rule_id == Contrast::Agent::Protect::Rule::Sqli::NAME
|
|
249
|
+
# Block At Perimeter mode has been deprecated in sqli_worth_watching_v2
|
|
250
|
+
# and should be treated equivalent to Blocked mode if set
|
|
251
|
+
Contrast::Api::Dtm::AttackResult::ResponseType::BLOCKED
|
|
252
|
+
else
|
|
253
|
+
Contrast::Api::Dtm::AttackResult::ResponseType::BLOCKED_AT_PERIMETER
|
|
254
|
+
end
|
|
236
255
|
log_rule_matched(context, ia_result, result.response)
|
|
237
256
|
elsif ia_result.nil? || ia_result.attack_count.zero?
|
|
238
257
|
result.response = Contrast::Api::Dtm::AttackResult::ResponseType::PROBED
|
|
@@ -293,6 +312,14 @@ module Contrast
|
|
|
293
312
|
result: response)
|
|
294
313
|
end
|
|
295
314
|
|
|
315
|
+
# This method returns the symbol for the enum
|
|
316
|
+
#
|
|
317
|
+
# @param enum [Enumerable]
|
|
318
|
+
# @return [Symbol]
|
|
319
|
+
def extract_input_type enum
|
|
320
|
+
Contrast::Api::Dtm::UserInput::InputType.get_name_by_tag enum
|
|
321
|
+
end
|
|
322
|
+
|
|
296
323
|
private
|
|
297
324
|
|
|
298
325
|
def log_rule_probed _context, ia_result
|
|
@@ -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
|
|
@@ -10,6 +11,8 @@ module Contrast
|
|
|
10
11
|
# Encapsulate common code for protect rules that do their
|
|
11
12
|
# input analysis on Speedracer rather in ruby code
|
|
12
13
|
class BaseService < Contrast::Agent::Protect::Rule::Base
|
|
14
|
+
include Contrast::Components::Logger::InstanceMethods
|
|
15
|
+
|
|
13
16
|
def rule_name
|
|
14
17
|
'base-service'
|
|
15
18
|
end
|
|
@@ -41,6 +44,7 @@ module Contrast
|
|
|
41
44
|
result = find_postfilter_attacker(context, nil)
|
|
42
45
|
return unless result&.samples&.any?
|
|
43
46
|
|
|
47
|
+
cef_logging result
|
|
44
48
|
append_to_activity(context, result)
|
|
45
49
|
return unless result.response == :BLOCKED
|
|
46
50
|
|
|
@@ -83,7 +87,12 @@ module Contrast
|
|
|
83
87
|
def find_postfilter_attacker context, potential_attack_string, **kwargs
|
|
84
88
|
ia_results = gather_ia_results(context)
|
|
85
89
|
ia_results.select! do |ia_result|
|
|
86
|
-
ia_result.score_level == Contrast::
|
|
90
|
+
ia_result.score_level == if ia_result.rule_id == Contrast::Agent::Protect::Rule::Sqli::NAME
|
|
91
|
+
Contrast::Agent::Reporting::ScoreLevel::WORTHWATCHING
|
|
92
|
+
else
|
|
93
|
+
# legacy implementation for DEFINITEATATACK
|
|
94
|
+
Contrast::Api::Settings::InputAnalysisResult::ScoreLevel::DEFINITEATTACK
|
|
95
|
+
end
|
|
87
96
|
end
|
|
88
97
|
find_attacker_with_results(context, potential_attack_string, ia_results, **kwargs)
|
|
89
98
|
end
|