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
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#include <ruby.h>
|
|
2
|
+
|
|
3
|
+
/* Calls to Contrast modules and classes */
|
|
4
|
+
VALUE scope_interface;
|
|
5
|
+
VALUE scope_inst_methods, scope_mod, scope_klass;
|
|
6
|
+
|
|
7
|
+
/* IVs */
|
|
8
|
+
static VALUE rb_iv_cntr_scope;
|
|
9
|
+
static VALUE rb_iv_dslr_scope;
|
|
10
|
+
static VALUE rb_iv_split_scope;
|
|
11
|
+
|
|
12
|
+
/* Constants */
|
|
13
|
+
static VALUE rb_const_ec;
|
|
14
|
+
static VALUE rb_const_mon;
|
|
15
|
+
static VALUE rb_const_ec_keys;
|
|
16
|
+
|
|
17
|
+
/* Symbols */
|
|
18
|
+
static VALUE rb_sym_scope_mod;
|
|
19
|
+
static VALUE rb_sym_contrast;
|
|
20
|
+
static VALUE rb_sym_deserialization;
|
|
21
|
+
static VALUE rb_sym_split;
|
|
22
|
+
|
|
23
|
+
/* methods names - only reusable ones */
|
|
24
|
+
VALUE rb_method_name_init;
|
|
25
|
+
VALUE rb_method_name_in_scope;
|
|
26
|
+
VALUE rb_method_name_enter_scope;
|
|
27
|
+
VALUE rb_method_name_exit_scope;
|
|
28
|
+
VALUE rb_method_name_scope_for_current_ec;
|
|
29
|
+
VALUE rb_method_name_in_cntr_scope;
|
|
30
|
+
VALUE rb_method_name_enter_cntr_scope;
|
|
31
|
+
VALUE rb_method_name_exit_cntr_scope;
|
|
32
|
+
VALUE rb_method_name_in_dslr_scope;
|
|
33
|
+
VALUE rb_method_name_enter_dslr_scope;
|
|
34
|
+
VALUE rb_method_name_exit_dslr_scope;
|
|
35
|
+
VALUE rb_method_name_in_split_scope;
|
|
36
|
+
VALUE rb_method_name_enter_split_scope;
|
|
37
|
+
VALUE rb_method_name_exit_split_scope;
|
|
38
|
+
VALUE rb_method_name_split_scope_depth;
|
|
39
|
+
|
|
40
|
+
/* Scope class */
|
|
41
|
+
VALUE contrast_scope_klass_init(VALUE self, VALUE args);
|
|
42
|
+
VALUE in_cntr_scope(VALUE self, VALUE args);
|
|
43
|
+
VALUE enter_cntr_scope(VALUE self, VALUE args);
|
|
44
|
+
VALUE exit_cntr_scope(VALUE self, VALUE args);
|
|
45
|
+
VALUE in_split_scope(VALUE self, VALUE args);
|
|
46
|
+
VALUE enter_split_scope(VALUE self, VALUE args);
|
|
47
|
+
VALUE exit_split_scope(VALUE self, VALUE args);
|
|
48
|
+
VALUE split_scope_depth(VALUE self, VALUE args);
|
|
49
|
+
VALUE in_dsrl_scope_scope(VALUE self, VALUE args);
|
|
50
|
+
VALUE enter_dsrl_scope(VALUE self, VALUE args);
|
|
51
|
+
VALUE exit_dsrl_scope(VALUE self, VALUE args);
|
|
52
|
+
VALUE scope_klass_in_scope(VALUE self, VALUE method_scope_sym);
|
|
53
|
+
VALUE scope_klass_enter_scope(VALUE self, VALUE method_scope_sym);
|
|
54
|
+
VALUE scope_klass_exit_scope(VALUE self, VALUE method_scope_sym);
|
|
55
|
+
|
|
56
|
+
/* Scope interface */
|
|
57
|
+
VALUE contrast_scope_interface_init(VALUE self, VALUE args);
|
|
58
|
+
VALUE contrast_scope_for_current_ec(VALUE self, VALUE args);
|
|
59
|
+
|
|
60
|
+
/* Scope instance methods */
|
|
61
|
+
VALUE inst_methods_in_cntr_scope(VALUE self, VALUE args);
|
|
62
|
+
VALUE inst_methods_enter_cntr_scope(VALUE self, VALUE args);
|
|
63
|
+
VALUE inst_methods_exit_cntr_scope(VALUE self, VALUE args);
|
|
64
|
+
VALUE inst_methods_in_split_scope(VALUE self, VALUE args);
|
|
65
|
+
VALUE inst_methods_enter_split_scope(VALUE self, VALUE args);
|
|
66
|
+
VALUE inst_methods_exit_split_scope(VALUE self, VALUE args);
|
|
67
|
+
VALUE inst_methods_split_scope_depth(VALUE self, VALUE args);
|
|
68
|
+
VALUE inst_methods_in_dsrl_scope(VALUE self, VALUE args);
|
|
69
|
+
VALUE inst_methods_enter_dsrl_scope(VALUE self, VALUE args);
|
|
70
|
+
VALUE inst_methods_exit_dsrl_scope(VALUE self, VALUE args);
|
|
71
|
+
VALUE inst_methods_in_scope(VALUE self, VALUE method_scope_sym);
|
|
72
|
+
VALUE inst_methods_enter_scope(VALUE self, VALUE method_scope_sym);
|
|
73
|
+
VALUE inst_methods_exit_scope(VALUE self, VALUE method_scope_sym);
|
|
74
|
+
|
|
75
|
+
/* Scope components module */
|
|
76
|
+
VALUE scope_mod_sweep_dead_ecs(VALUE self, VALUE args);
|
|
77
|
+
VALUE inst_methods_enter_method_scope(VALUE self, VALUE scopes_to_enter);
|
|
78
|
+
VALUE inst_methods_exit_method_scope(VALUE self, VALUE scopes_to_exit);
|
|
79
|
+
|
|
80
|
+
/* Helpers */
|
|
81
|
+
VALUE is_in_scope(int scope);
|
|
82
|
+
VALUE get_ec();
|
|
83
|
+
VALUE rb_new_c_scope();
|
|
84
|
+
void rb_raise_scope_no_method_err(const VALUE method_scope_sym);
|
|
85
|
+
int scope_increase(int scope);
|
|
86
|
+
int scope_decrease(int scope);
|
|
87
|
+
|
|
88
|
+
void Init_cs__scope(void);
|
|
@@ -15,20 +15,27 @@ module Contrast
|
|
|
15
15
|
module Assess
|
|
16
16
|
# This class holds the data about an event in the application We'll use it to build an event that TeamServer can
|
|
17
17
|
# consume if the object to which this event belongs ends in a trigger.
|
|
18
|
-
#
|
|
19
|
-
# @attr_reader event_id [Integer] the atomic id of this event
|
|
20
|
-
# @attr_reader policy_node [Contrast::Agent::Assess::Policy::PolicyNode] the node that governs this event.
|
|
21
|
-
# @attr_reader stack_trace [Array<String>] the execution stack at the time the method for this event was invoked
|
|
22
|
-
# @attr_reader time [Integer] the time, in epoch ms, when this event was created
|
|
23
|
-
# @attr_reader thread [Integer] the object id of the thread on which this event was generated
|
|
24
|
-
# @attr_reader object [Contrast::Agent::Assess::ContrastObject] the safe representation of the Object on which
|
|
25
|
-
# the method was invoked
|
|
26
|
-
# @attr_reader ret [Contrast::Agent::Assess::ContrastObject] the safe representation of the Return of the invoked
|
|
27
|
-
# method
|
|
28
|
-
# @attr_reader args [Array<Contrast::Agent::Assess::ContrastObject>] the safe representation of the Arguments
|
|
29
|
-
# with which the method was invoked
|
|
30
18
|
class ContrastEvent
|
|
31
|
-
|
|
19
|
+
# @return [Integer] the atomic id of this event
|
|
20
|
+
attr_reader :event_id
|
|
21
|
+
# @return [Contrast::Agent::Assess::Policy::PolicyNode] the node that governs this event.
|
|
22
|
+
attr_reader :policy_node
|
|
23
|
+
# @return [Array<String>] the execution stack at the time the method for this event was invoked
|
|
24
|
+
attr_reader :stack_trace
|
|
25
|
+
# @return [Integer] the time, in epoch ms, when this event was created
|
|
26
|
+
attr_reader :time
|
|
27
|
+
# @return [Integer] the object id of the thread on which this event was generated
|
|
28
|
+
attr_reader :thread
|
|
29
|
+
# @return [Contrast::Agent::Assess::ContrastObject] the safe representation of the Object on which the method
|
|
30
|
+
# was invoked
|
|
31
|
+
attr_reader :object
|
|
32
|
+
# @return [Contrast::Agent::Assess::ContrastObject] the safe representation of the Return of the invoked method
|
|
33
|
+
attr_reader :ret
|
|
34
|
+
# @return [Array<Contrast::Agent::Assess::ContrastObject, nil>] the safe representation of the Arguments with
|
|
35
|
+
# which the method was invoked
|
|
36
|
+
attr_reader :args
|
|
37
|
+
# @return [Hash<Contrast::Agent::Assess::Tag>]
|
|
38
|
+
attr_reader :tags
|
|
32
39
|
|
|
33
40
|
# We need this to track the parent id's of events to build up a flow chart of the finding
|
|
34
41
|
@atomic_id = 0
|
|
@@ -118,11 +118,8 @@ module Contrast
|
|
|
118
118
|
# it is an interesting security event that has a meaningful
|
|
119
119
|
# change.
|
|
120
120
|
def tagger?
|
|
121
|
-
@_tagger
|
|
122
|
-
|
|
123
|
-
has_untags = untags&.any?
|
|
124
|
-
has_tags || has_untags
|
|
125
|
-
end
|
|
121
|
+
@_tagger = tags&.any? || untags&.any? if @_tagger.nil?
|
|
122
|
+
@_tagger
|
|
126
123
|
end
|
|
127
124
|
end
|
|
128
125
|
end
|
|
@@ -8,6 +8,9 @@ require 'contrast/utils/object_share'
|
|
|
8
8
|
require 'contrast/utils/sha256_builder'
|
|
9
9
|
require 'contrast/utils/assess/trigger_method_utils'
|
|
10
10
|
require 'contrast/agent/assess/events/event_data'
|
|
11
|
+
require 'contrast/agent/reporting/reporting_events/preflight'
|
|
12
|
+
require 'contrast/agent/reporting/reporting_events/preflight_message'
|
|
13
|
+
require 'contrast/agent/reporting/reporting_events/route_discovery'
|
|
11
14
|
require 'contrast/agent/reporting/reporting_utilities/reporting_storage'
|
|
12
15
|
|
|
13
16
|
module Contrast
|
|
@@ -154,7 +157,7 @@ module Contrast
|
|
|
154
157
|
|
|
155
158
|
new_preflight = Contrast::Agent::Reporting::Preflight.new
|
|
156
159
|
new_preflight_message = Contrast::Agent::Reporting::PreflightMessage.new
|
|
157
|
-
new_preflight_message.routes << request
|
|
160
|
+
new_preflight_message.routes << Contrast::Agent::Reporting::RouteDiscovery.convert(request.route)
|
|
158
161
|
new_preflight_message.hash_code = hash_code
|
|
159
162
|
new_preflight_message.data = "#{ trigger_node.rule_id },#{ hash_code }"
|
|
160
163
|
new_preflight.messages << new_preflight_message
|
|
@@ -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/body_rule'
|
|
5
5
|
require 'contrast/utils/string_utils'
|
|
6
6
|
|
|
7
7
|
module Contrast
|
|
@@ -11,7 +11,8 @@ module Contrast
|
|
|
11
11
|
module Response
|
|
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 autocomplete attribute.
|
|
14
|
-
class
|
|
14
|
+
class AutoComplete < BaseRule
|
|
15
|
+
include BodyRule
|
|
15
16
|
def rule_id
|
|
16
17
|
'autocomplete-missing'
|
|
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.
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require 'rack'
|
|
5
|
+
require 'json'
|
|
5
6
|
require 'contrast/agent/reporting/reporting_utilities/dtm_message'
|
|
6
7
|
require 'contrast/utils/hash_digest'
|
|
7
8
|
require 'contrast/utils/preflight_util'
|
|
@@ -41,9 +42,6 @@ module Contrast
|
|
|
41
42
|
protected
|
|
42
43
|
|
|
43
44
|
DATA = 'data'.cs__freeze
|
|
44
|
-
HTML_PROP = 'html'.cs__freeze
|
|
45
|
-
START_PROP = 'start'.cs__freeze
|
|
46
|
-
END_PROP = 'end'.cs__freeze
|
|
47
45
|
|
|
48
46
|
# Rules discern which responses they can/should analyze.
|
|
49
47
|
#
|
|
@@ -60,10 +58,15 @@ module Contrast
|
|
|
60
58
|
|
|
61
59
|
# Determine if the Response violates the Rule or not. If it does, return the evidence that proves it so.
|
|
62
60
|
#
|
|
63
|
-
# @param
|
|
61
|
+
# @param response [Contrast::Agent::Response] the response of the application
|
|
64
62
|
# @return [Hash, nil] the evidence required to prove the violation of the rule
|
|
65
63
|
def violated? _response; end
|
|
66
64
|
|
|
65
|
+
def evidence data = Contrast::Utils::ObjectShare::EMPTY_STRING
|
|
66
|
+
data = Contrast::Utils::ObjectShare::EMPTY_STRING if data.nil?
|
|
67
|
+
{ DATA => data }
|
|
68
|
+
end
|
|
69
|
+
|
|
67
70
|
# Convert the given evidence into a finding. The rule will populate this evidence with each of the
|
|
68
71
|
#
|
|
69
72
|
# @param evidence [Hash] the properties required to build this finding.
|
|
@@ -87,7 +90,11 @@ module Contrast
|
|
|
87
90
|
# @param finding [Contrast::Api::Dtm::Finding] finding to attach the evidence to
|
|
88
91
|
def build_evidence evidence, finding
|
|
89
92
|
evidence.each_pair do |key, value|
|
|
90
|
-
finding.properties[key] =
|
|
93
|
+
finding.properties[key] = if value.cs__is_a?(Hash)
|
|
94
|
+
Contrast::Utils::StringUtils.protobuf_format(value.to_json)
|
|
95
|
+
else
|
|
96
|
+
Contrast::Utils::StringUtils.protobuf_format(value)
|
|
97
|
+
end
|
|
91
98
|
end
|
|
92
99
|
end
|
|
93
100
|
|
|
@@ -115,80 +122,6 @@ module Contrast
|
|
|
115
122
|
def valid_content_type? type
|
|
116
123
|
!type || [/json/i, /xml/i].none? { |invalid_content| type =~ invalid_content }
|
|
117
124
|
end
|
|
118
|
-
|
|
119
|
-
# Determine if a response has a body or not.
|
|
120
|
-
#
|
|
121
|
-
# @param response [Contrast::Agent::Response] the response of the application
|
|
122
|
-
# @return [Boolean]
|
|
123
|
-
def body? response
|
|
124
|
-
Contrast::Utils::StringUtils.present?(response.body)
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
# Capture the information needed to build the properties of this finding by parsing out from the body
|
|
128
|
-
#
|
|
129
|
-
# @param body [String] the entire HTTP Response body
|
|
130
|
-
# @param body_start [Integer] the start of the range to take from the body
|
|
131
|
-
# @param body_close [Integer] the end of the range to take from the body
|
|
132
|
-
# @param tag_stop [Integer] the index of the end of the html tag from its start
|
|
133
|
-
# @return [Hash]
|
|
134
|
-
def capture body, body_start, body_close, tag_stop
|
|
135
|
-
tag = {}
|
|
136
|
-
# Capture the 50 characters in front of the form, or up to the start if the form starts before 50.
|
|
137
|
-
capture_start = body_start < 50 ? 0 : body_start - 50
|
|
138
|
-
# Start is where the '<form' is in the body
|
|
139
|
-
# 6 accounts for the characters in the form and the opening char
|
|
140
|
-
# potential_form.index('>') accounts for finding the rest of the form
|
|
141
|
-
# 50 accounts for the context to capture beyond
|
|
142
|
-
capture_close = body_close + 50
|
|
143
|
-
tag[HTML_PROP] = body[capture_start...capture_close]
|
|
144
|
-
tag[START_PROP] = body_start < 50 ? body_start : 50
|
|
145
|
-
tag[END_PROP] = tag[START_PROP] + 6 + tag_stop
|
|
146
|
-
tag
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
# Find the forms in this body, if any, so as to determine if they violate this rule.
|
|
150
|
-
#
|
|
151
|
-
# @param body [String]
|
|
152
|
-
# @return [Array<Hash>] the forms of this body, as well as their start and end indexes.
|
|
153
|
-
def forms body
|
|
154
|
-
forms = []
|
|
155
|
-
body_start = 0
|
|
156
|
-
# The instance of "<form" in the body may be a form. Turn them into chunks to check.
|
|
157
|
-
potential_forms = body.split(form_start)
|
|
158
|
-
potential_forms.each do |potential_form|
|
|
159
|
-
# We can consider this a form if the next character is one of whitespace of form tag closing
|
|
160
|
-
# characters.
|
|
161
|
-
next unless potential_form
|
|
162
|
-
next unless form_openings.any? { |opening| potential_form.start_with?(opening) }
|
|
163
|
-
|
|
164
|
-
body_start = body.index(form_start, body_start)
|
|
165
|
-
next unless body_start
|
|
166
|
-
|
|
167
|
-
form_stop = potential_form.index('>').to_i
|
|
168
|
-
next unless form_stop
|
|
169
|
-
|
|
170
|
-
body_close = body_start + 6 + form_stop
|
|
171
|
-
forms << capture(body, body_start, body_close, form_stop)
|
|
172
|
-
body_start = body_close
|
|
173
|
-
end
|
|
174
|
-
forms
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
def form_start
|
|
178
|
-
/<form/i
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
def form_openings
|
|
182
|
-
[' ', "\n", "\r", "\t", '>']
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
# Determine if a response has headers.
|
|
186
|
-
#
|
|
187
|
-
# @param response [Contrast::Agent::Response] the response of the application
|
|
188
|
-
# @return [Boolean]
|
|
189
|
-
def headers? response
|
|
190
|
-
response.headers.cs__is_a?(Hash)
|
|
191
|
-
end
|
|
192
125
|
end
|
|
193
126
|
end
|
|
194
127
|
end
|
|
@@ -0,0 +1,109 @@
|
|
|
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
|
+
module BodyRule
|
|
19
|
+
protected
|
|
20
|
+
|
|
21
|
+
HTML_PROP = 'html'.cs__freeze
|
|
22
|
+
START_PROP = 'start'.cs__freeze
|
|
23
|
+
END_PROP = 'end'.cs__freeze
|
|
24
|
+
FORM_START_REGEXP = /<form/i.cs__freeze
|
|
25
|
+
META_TYPE = 'meta'
|
|
26
|
+
PRAGMA = 'pragma'
|
|
27
|
+
|
|
28
|
+
# Rules discern which responses they can/should analyze.
|
|
29
|
+
#
|
|
30
|
+
# @param response [Contrast::Agent::Response] the response of the application
|
|
31
|
+
def analyze_response? response
|
|
32
|
+
super && body?(response)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Determine if a response has a body or not.
|
|
36
|
+
#
|
|
37
|
+
# @param response [Contrast::Agent::Response] the response of the application
|
|
38
|
+
# @return [Boolean]
|
|
39
|
+
def body? response
|
|
40
|
+
Contrast::Utils::StringUtils.present?(response.body)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Find the elements in this section, if any, so as to determine if they violate this rule.
|
|
44
|
+
#
|
|
45
|
+
# @param section [String,nil] html section to find element
|
|
46
|
+
# @param element_start_str [String] element to find in html section
|
|
47
|
+
# @return [Array<Hash>] the found elements of this section, as well as their start and end indexes.
|
|
48
|
+
def html_elements section, element_start_str = '', capture_overflow = false
|
|
49
|
+
elements = []
|
|
50
|
+
section_start = 0
|
|
51
|
+
return [] unless section
|
|
52
|
+
|
|
53
|
+
potential_elements(section, element_start_str).flatten.each do |potential_element|
|
|
54
|
+
next unless potential_element
|
|
55
|
+
next unless element_openings.any? { |opening| potential_element.starts_with?(opening) }
|
|
56
|
+
|
|
57
|
+
section_start = section.index(element_start_str, section_start)
|
|
58
|
+
next unless section_start
|
|
59
|
+
|
|
60
|
+
element_stop = potential_element.index('>').to_i
|
|
61
|
+
next unless element_stop
|
|
62
|
+
|
|
63
|
+
section_close = section_start + 6 + element_stop
|
|
64
|
+
elements << capture(section, section_start, section_close, element_stop, capture_overflow)
|
|
65
|
+
section_start = section_close
|
|
66
|
+
end
|
|
67
|
+
elements
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def potential_elements section, element_start
|
|
71
|
+
section.split(element_start)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def element_openings
|
|
75
|
+
[' ', "\n", "\r", "\t", '>']
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Capture the information needed to build the properties of this finding by parsing out from the body
|
|
79
|
+
#
|
|
80
|
+
# @param body [String] the entire HTTP Response body
|
|
81
|
+
# @param body_start [Integer] the start of the range to take from the body
|
|
82
|
+
# @param body_close [Integer] the end of the range to take from the body
|
|
83
|
+
# @param tag_stop [Integer] the index of the end of the html tag from its start
|
|
84
|
+
# @return [Hash]
|
|
85
|
+
def capture body, body_start, body_close, tag_stop, overflow = false
|
|
86
|
+
tag = {}
|
|
87
|
+
# Capture the 50 characters in front of the form, or up to the start if the form starts before 50.
|
|
88
|
+
if overflow
|
|
89
|
+
capture_start = body_start < 50 ? 0 : body_start - 50
|
|
90
|
+
# Start is where the '<form' is in the body
|
|
91
|
+
# 6 accounts for the characters in the form and the opening char
|
|
92
|
+
# potential_form.index('>') accounts for finding the rest of the form
|
|
93
|
+
# 50 accounts for the context to capture beyond
|
|
94
|
+
capture_close = body_close + 50
|
|
95
|
+
tag[HTML_PROP] = body[capture_start...capture_close]
|
|
96
|
+
tag[START_PROP] = body_start < 50 ? body_start : 50
|
|
97
|
+
else
|
|
98
|
+
tag[HTML_PROP] = body[body_start...body_close]
|
|
99
|
+
tag[START_PROP] = body_start
|
|
100
|
+
end
|
|
101
|
+
tag[END_PROP] = tag[START_PROP] + 6 + tag_stop
|
|
102
|
+
tag
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,157 @@
|
|
|
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/agent/assess/rule/response/body_rule'
|
|
6
|
+
require 'contrast/agent/assess/rule/response/framework/rails_support'
|
|
7
|
+
require 'contrast/utils/string_utils'
|
|
8
|
+
require 'json'
|
|
9
|
+
|
|
10
|
+
module Contrast
|
|
11
|
+
module Agent
|
|
12
|
+
module Assess
|
|
13
|
+
module Rule
|
|
14
|
+
module Response
|
|
15
|
+
# These rules check the content of the HTTP Response to determine if the body or the headers include and/or
|
|
16
|
+
# set incorrectly the cache-control header
|
|
17
|
+
class CacheControl < HeaderRule
|
|
18
|
+
include BodyRule
|
|
19
|
+
include Framework::RailsSupport
|
|
20
|
+
|
|
21
|
+
def rule_id
|
|
22
|
+
'cache-controls-missing'
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
protected
|
|
26
|
+
|
|
27
|
+
HEADER_KEYS = %w[Cache-Control].cs__freeze
|
|
28
|
+
ACCEPTED_VALUES = [/no-store/, /no-cache/].cs__freeze
|
|
29
|
+
DEFAULT_SAFE = false
|
|
30
|
+
META_START_STR = /<meta/i.cs__freeze
|
|
31
|
+
HEAD_TAG = /<head>/i.cs__freeze
|
|
32
|
+
NAME = 'cache-control'
|
|
33
|
+
|
|
34
|
+
# Determine if the Response violates the Rule or not. If it does, return the evidence that proves it so.
|
|
35
|
+
#
|
|
36
|
+
# @param response [Contrast::Agent::Response] the response of the application
|
|
37
|
+
# @return [Hash, nil] the evidence required to prove the violation of the rule
|
|
38
|
+
def violated? response
|
|
39
|
+
has_header, evidence = process_header(response)
|
|
40
|
+
return evidence unless evidence.nil?
|
|
41
|
+
|
|
42
|
+
has_tag, evidence = process_body(response)
|
|
43
|
+
return evidence unless evidence.nil?
|
|
44
|
+
return {} if !has_header && !has_tag
|
|
45
|
+
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Process Header value to determine if it violates rule
|
|
50
|
+
# @param response [Contrast::Agent::Response] the response of the application
|
|
51
|
+
# @return [Array[Boolean, Hash]] whether the header exists and the evidence hash or nil
|
|
52
|
+
def process_header response
|
|
53
|
+
# Rails 7 adds support for the cache_control header directly in the
|
|
54
|
+
# rack response, we should use that value
|
|
55
|
+
if framework_supported?
|
|
56
|
+
cache_control = response.rack_response.cache_control
|
|
57
|
+
has_header = !cache_control.empty?
|
|
58
|
+
not_valid = has_header && !((cache_control[:no_cache]) || (cache_control[:no_store]))
|
|
59
|
+
# evidence requires header value string, pull directly instead of rebuilding from hash
|
|
60
|
+
return has_header, evidence(HEADER_TYPE, NAME, cache_control_to_s(cache_control)) if not_valid
|
|
61
|
+
else
|
|
62
|
+
# This rule is violated if the header is not there is there,
|
|
63
|
+
# but the value is not 'no-store' or 'no-cache'
|
|
64
|
+
cache_control = get_header_value(response)
|
|
65
|
+
has_header = !!cache_control
|
|
66
|
+
not_valid = has_header && !valid_header?(cache_control)
|
|
67
|
+
return has_header, evidence(HEADER_TYPE, NAME, cache_control) if not_valid
|
|
68
|
+
end
|
|
69
|
+
[has_header, nil]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Process Body to determine cache control exists as meta tag and if it violates rule
|
|
73
|
+
# @param response [Contrast::Agent::Response] the response of the application
|
|
74
|
+
# @return [Array[Boolean, Hash]] whether the meta tags exists and the evidence hash or nil
|
|
75
|
+
def process_body response
|
|
76
|
+
body = response.body
|
|
77
|
+
# check if the meta tag is include it
|
|
78
|
+
meta_tags = html_elements(body&.split(HEAD_TAG)&.last, META_START_STR)
|
|
79
|
+
meta_tags.each do |tag|
|
|
80
|
+
return true, evidence(META_TYPE, PRAGMA, tag[HTML_PROP]) if meta_cache_tag? tag[HTML_PROP]
|
|
81
|
+
end
|
|
82
|
+
[!meta_tags.empty?, nil]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def potential_elements section, element_start
|
|
86
|
+
section.split(element_start)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def accepted_http_values
|
|
90
|
+
[/'cache-control'/i, /"cache-control"/i]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def accepted_values
|
|
94
|
+
[/'no-cache'/i, /"no-cache"/i, /"no-store"/i, /'no-store'/i, /'cache-control'/i, /"cache-control"/i]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Determine if the given metatag does not have a valid cache-control tag.
|
|
98
|
+
# Meta tags has the option to set http-equiv and content to set the http response header
|
|
99
|
+
# to define for the document
|
|
100
|
+
#
|
|
101
|
+
# @param tag [String] the meta tag
|
|
102
|
+
# @return [Boolean, nil]
|
|
103
|
+
def meta_cache_tag? tag
|
|
104
|
+
# Here we should determine the index of the needed keys
|
|
105
|
+
# http-equiv and content
|
|
106
|
+
http_equiv_idx = tag =~ /http-equiv=/i
|
|
107
|
+
return false unless http_equiv_idx
|
|
108
|
+
|
|
109
|
+
content_idx = tag =~ /content=/i
|
|
110
|
+
return false unless content_idx
|
|
111
|
+
|
|
112
|
+
# determine the value of the http-equiv if it's cache-control
|
|
113
|
+
http_equiv_idx += 11
|
|
114
|
+
is_valid = accepted_http_values.any? { |el| (tag =~ el) == http_equiv_idx }
|
|
115
|
+
return false unless is_valid
|
|
116
|
+
|
|
117
|
+
content_idx += 8
|
|
118
|
+
return false if accepted_values.any? { |value| (tag =~ value) == content_idx }
|
|
119
|
+
|
|
120
|
+
true
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# This method accepts the violation and transforms it to the proper hash
|
|
124
|
+
# before return in as violation
|
|
125
|
+
#
|
|
126
|
+
# @param type [String] String of Header or META of the type
|
|
127
|
+
# @param name [String] String of either cache-control or pragma
|
|
128
|
+
# @param value [String] String of the violated value
|
|
129
|
+
def evidence type, name, value
|
|
130
|
+
{ DATA => { type: type, name: name, value: value }.to_s }
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Rebuilds the String value of the Cache-Control Header
|
|
134
|
+
# from the hash build in the Rack::Response
|
|
135
|
+
#
|
|
136
|
+
# @param hsh [Hash]
|
|
137
|
+
# @return [String]
|
|
138
|
+
def cache_control_to_s hsh
|
|
139
|
+
values = []
|
|
140
|
+
hsh.each_pair do |k, v|
|
|
141
|
+
key = k.to_s.tr('_', '-')
|
|
142
|
+
values << if key.to_sym == :extras
|
|
143
|
+
v
|
|
144
|
+
elsif v.is_a?(TrueClass)
|
|
145
|
+
key
|
|
146
|
+
else
|
|
147
|
+
"#{ key }=#{ v }"
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
values.join(', ')
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -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 headers contains the required header
|
|
13
|
+
class ClickJacking < HeaderRule
|
|
14
|
+
def rule_id
|
|
15
|
+
'clickjacking-control-missing'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
HEADER_KEYS = %w[X-Frame-Options].cs__freeze
|
|
19
|
+
ACCEPTED_VALUES = [/^deny/i, /^sameorigin/i].cs__freeze
|
|
20
|
+
DEFAULT_SAFE = false
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|