actionpack 5.2.8.1 → 6.1.6.1
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of actionpack might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +383 -346
- data/MIT-LICENSE +1 -2
- data/README.rdoc +4 -3
- data/lib/abstract_controller/base.rb +38 -4
- data/lib/abstract_controller/caching/fragments.rb +6 -22
- data/lib/abstract_controller/caching.rb +1 -1
- data/lib/abstract_controller/callbacks.rb +14 -2
- data/lib/abstract_controller/collector.rb +5 -4
- data/lib/abstract_controller/helpers.rb +106 -90
- data/lib/abstract_controller/railties/routes_helpers.rb +17 -1
- data/lib/abstract_controller/rendering.rb +9 -9
- data/lib/abstract_controller/translation.rb +11 -5
- data/lib/abstract_controller.rb +1 -0
- data/lib/action_controller/api.rb +4 -3
- data/lib/action_controller/base.rb +6 -9
- data/lib/action_controller/caching.rb +1 -3
- data/lib/action_controller/log_subscriber.rb +10 -7
- data/lib/action_controller/metal/basic_implicit_render.rb +1 -1
- data/lib/action_controller/metal/conditional_get.rb +19 -5
- data/lib/action_controller/metal/content_security_policy.rb +1 -2
- data/lib/action_controller/metal/cookies.rb +3 -1
- data/lib/action_controller/metal/data_streaming.rb +6 -7
- data/lib/action_controller/metal/default_headers.rb +17 -0
- data/lib/action_controller/metal/etag_with_template_digest.rb +4 -6
- data/lib/action_controller/metal/exceptions.rb +56 -2
- data/lib/action_controller/metal/flash.rb +5 -5
- data/lib/action_controller/metal/head.rb +7 -4
- data/lib/action_controller/metal/helpers.rb +14 -5
- data/lib/action_controller/metal/http_authentication.rb +25 -23
- data/lib/action_controller/metal/implicit_render.rb +5 -15
- data/lib/action_controller/metal/instrumentation.rb +13 -14
- data/lib/action_controller/metal/live.rb +39 -32
- data/lib/action_controller/metal/logging.rb +20 -0
- data/lib/action_controller/metal/mime_responds.rb +19 -4
- data/lib/action_controller/metal/parameter_encoding.rb +35 -4
- data/lib/action_controller/metal/params_wrapper.rb +32 -22
- data/lib/action_controller/metal/permissions_policy.rb +46 -0
- data/lib/action_controller/metal/redirecting.rb +6 -6
- data/lib/action_controller/metal/renderers.rb +4 -4
- data/lib/action_controller/metal/rendering.rb +8 -3
- data/lib/action_controller/metal/request_forgery_protection.rb +26 -49
- data/lib/action_controller/metal/rescue.rb +1 -1
- data/lib/action_controller/metal/streaming.rb +0 -1
- data/lib/action_controller/metal/strong_parameters.rb +168 -59
- data/lib/action_controller/metal/url_for.rb +1 -1
- data/lib/action_controller/metal.rb +10 -8
- data/lib/action_controller/railties/helpers.rb +1 -1
- data/lib/action_controller/renderer.rb +37 -13
- data/lib/action_controller/template_assertions.rb +1 -1
- data/lib/action_controller/test_case.rb +71 -63
- data/lib/action_controller.rb +7 -4
- data/lib/action_dispatch/http/cache.rb +31 -27
- data/lib/action_dispatch/http/content_disposition.rb +45 -0
- data/lib/action_dispatch/http/content_security_policy.rb +34 -18
- data/lib/action_dispatch/http/filter_parameters.rb +9 -8
- data/lib/action_dispatch/http/filter_redirect.rb +2 -3
- data/lib/action_dispatch/http/headers.rb +4 -4
- data/lib/action_dispatch/http/mime_negotiation.rb +26 -13
- data/lib/action_dispatch/http/mime_type.rb +43 -24
- data/lib/action_dispatch/http/parameters.rb +14 -23
- data/lib/action_dispatch/http/permissions_policy.rb +173 -0
- data/lib/action_dispatch/http/request.rb +45 -22
- data/lib/action_dispatch/http/response.rb +45 -25
- data/lib/action_dispatch/http/upload.rb +9 -1
- data/lib/action_dispatch/http/url.rb +82 -82
- data/lib/action_dispatch/journey/formatter.rb +55 -31
- data/lib/action_dispatch/journey/gtg/builder.rb +22 -37
- data/lib/action_dispatch/journey/gtg/simulator.rb +8 -7
- data/lib/action_dispatch/journey/gtg/transition_table.rb +6 -5
- data/lib/action_dispatch/journey/nfa/dot.rb +0 -11
- data/lib/action_dispatch/journey/nodes/node.rb +13 -11
- data/lib/action_dispatch/journey/parser.rb +13 -13
- data/lib/action_dispatch/journey/parser.y +1 -1
- data/lib/action_dispatch/journey/path/pattern.rb +19 -21
- data/lib/action_dispatch/journey/route.rb +10 -20
- data/lib/action_dispatch/journey/router/utils.rb +14 -12
- data/lib/action_dispatch/journey/router.rb +26 -34
- data/lib/action_dispatch/journey/routes.rb +0 -2
- data/lib/action_dispatch/journey/scanner.rb +10 -4
- data/lib/action_dispatch/journey/visitors.rb +1 -4
- data/lib/action_dispatch/journey.rb +0 -2
- data/lib/action_dispatch/middleware/actionable_exceptions.rb +46 -0
- data/lib/action_dispatch/middleware/callbacks.rb +2 -4
- data/lib/action_dispatch/middleware/cookies.rb +128 -109
- data/lib/action_dispatch/middleware/debug_exceptions.rb +43 -66
- data/lib/action_dispatch/middleware/debug_locks.rb +5 -5
- data/lib/action_dispatch/middleware/debug_view.rb +66 -0
- data/lib/action_dispatch/middleware/exception_wrapper.rb +75 -30
- data/lib/action_dispatch/middleware/flash.rb +1 -1
- data/lib/action_dispatch/middleware/host_authorization.rb +170 -0
- data/lib/action_dispatch/middleware/public_exceptions.rb +6 -3
- data/lib/action_dispatch/middleware/remote_ip.rb +14 -16
- data/lib/action_dispatch/middleware/request_id.rb +5 -6
- data/lib/action_dispatch/middleware/session/abstract_store.rb +2 -3
- data/lib/action_dispatch/middleware/session/cookie_store.rb +3 -9
- data/lib/action_dispatch/middleware/show_exceptions.rb +13 -2
- data/lib/action_dispatch/middleware/ssl.rb +20 -15
- data/lib/action_dispatch/middleware/stack.rb +56 -2
- data/lib/action_dispatch/middleware/static.rb +153 -93
- data/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb +13 -0
- data/lib/action_dispatch/middleware/templates/rescues/_actions.text.erb +0 -0
- data/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb +22 -0
- data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +3 -1
- data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb +1 -1
- data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +4 -2
- data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +45 -35
- data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +7 -0
- data/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +5 -0
- data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +23 -4
- data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +1 -1
- data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +6 -3
- data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +4 -1
- data/lib/action_dispatch/middleware/templates/rescues/layout.erb +104 -8
- data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +19 -0
- data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb +3 -0
- data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +2 -2
- data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +1 -1
- data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +2 -2
- data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +1 -1
- data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +24 -1
- data/lib/action_dispatch/railtie.rb +8 -2
- data/lib/action_dispatch/request/session.rb +11 -10
- data/lib/action_dispatch/request/utils.rb +26 -2
- data/lib/action_dispatch/routing/inspector.rb +100 -52
- data/lib/action_dispatch/routing/mapper.rb +155 -103
- data/lib/action_dispatch/routing/polymorphic_routes.rb +13 -15
- data/lib/action_dispatch/routing/redirection.rb +4 -4
- data/lib/action_dispatch/routing/route_set.rb +71 -69
- data/lib/action_dispatch/routing/url_for.rb +2 -2
- data/lib/action_dispatch/routing.rb +21 -20
- data/lib/action_dispatch/system_test_case.rb +60 -11
- data/lib/action_dispatch/system_testing/browser.rb +53 -16
- data/lib/action_dispatch/system_testing/driver.rb +11 -3
- data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +49 -7
- data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +8 -10
- data/lib/action_dispatch/testing/assertion_response.rb +0 -1
- data/lib/action_dispatch/testing/assertions/response.rb +4 -7
- data/lib/action_dispatch/testing/assertions/routing.rb +20 -8
- data/lib/action_dispatch/testing/assertions.rb +1 -1
- data/lib/action_dispatch/testing/integration.rb +60 -28
- data/lib/action_dispatch/testing/request_encoder.rb +2 -2
- data/lib/action_dispatch/testing/test_process.rb +32 -4
- data/lib/action_dispatch/testing/test_request.rb +3 -3
- data/lib/action_dispatch/testing/test_response.rb +4 -32
- data/lib/action_dispatch.rb +9 -3
- data/lib/action_pack/gem_version.rb +3 -3
- data/lib/action_pack.rb +1 -1
- metadata +34 -21
- data/lib/action_controller/metal/force_ssl.rb +0 -99
- data/lib/action_dispatch/http/parameter_filter.rb +0 -86
- data/lib/action_dispatch/journey/nfa/builder.rb +0 -78
- data/lib/action_dispatch/journey/nfa/simulator.rb +0 -49
- data/lib/action_dispatch/journey/nfa/transition_table.rb +0 -120
- data/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb +0 -26
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pp"
|
4
|
+
|
5
|
+
require "action_view"
|
6
|
+
require "action_view/base"
|
7
|
+
|
8
|
+
module ActionDispatch
|
9
|
+
class DebugView < ActionView::Base # :nodoc:
|
10
|
+
RESCUES_TEMPLATE_PATH = File.expand_path("templates", __dir__)
|
11
|
+
|
12
|
+
def initialize(assigns)
|
13
|
+
paths = [RESCUES_TEMPLATE_PATH]
|
14
|
+
lookup_context = ActionView::LookupContext.new(paths)
|
15
|
+
super(lookup_context, assigns, nil)
|
16
|
+
end
|
17
|
+
|
18
|
+
def compiled_method_container
|
19
|
+
self.class
|
20
|
+
end
|
21
|
+
|
22
|
+
def debug_params(params)
|
23
|
+
clean_params = params.clone
|
24
|
+
clean_params.delete("action")
|
25
|
+
clean_params.delete("controller")
|
26
|
+
|
27
|
+
if clean_params.empty?
|
28
|
+
"None"
|
29
|
+
else
|
30
|
+
PP.pp(clean_params, +"", 200)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def debug_headers(headers)
|
35
|
+
if headers.present?
|
36
|
+
headers.inspect.gsub(",", ",\n")
|
37
|
+
else
|
38
|
+
"None"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def debug_hash(object)
|
43
|
+
object.to_hash.sort_by { |k, _| k.to_s }.map { |k, v| "#{k}: #{v.inspect rescue $!.message}" }.join("\n")
|
44
|
+
end
|
45
|
+
|
46
|
+
def render(*)
|
47
|
+
logger = ActionView::Base.logger
|
48
|
+
|
49
|
+
if logger && logger.respond_to?(:silence)
|
50
|
+
logger.silence { super }
|
51
|
+
else
|
52
|
+
super
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def protect_against_forgery?
|
57
|
+
false
|
58
|
+
end
|
59
|
+
|
60
|
+
def params_valid?
|
61
|
+
@request.parameters
|
62
|
+
rescue ActionController::BadRequest
|
63
|
+
false
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -6,44 +6,72 @@ require "rack/utils"
|
|
6
6
|
module ActionDispatch
|
7
7
|
class ExceptionWrapper
|
8
8
|
cattr_accessor :rescue_responses, default: Hash.new(:internal_server_error).merge!(
|
9
|
-
"ActionController::RoutingError"
|
10
|
-
"AbstractController::ActionNotFound"
|
11
|
-
"ActionController::MethodNotAllowed"
|
12
|
-
"ActionController::UnknownHttpMethod"
|
13
|
-
"ActionController::NotImplemented"
|
14
|
-
"ActionController::UnknownFormat"
|
15
|
-
"
|
16
|
-
"ActionController::
|
17
|
-
"
|
18
|
-
"ActionController::
|
19
|
-
"
|
20
|
-
"
|
21
|
-
"
|
9
|
+
"ActionController::RoutingError" => :not_found,
|
10
|
+
"AbstractController::ActionNotFound" => :not_found,
|
11
|
+
"ActionController::MethodNotAllowed" => :method_not_allowed,
|
12
|
+
"ActionController::UnknownHttpMethod" => :method_not_allowed,
|
13
|
+
"ActionController::NotImplemented" => :not_implemented,
|
14
|
+
"ActionController::UnknownFormat" => :not_acceptable,
|
15
|
+
"ActionDispatch::Http::MimeNegotiation::InvalidType" => :not_acceptable,
|
16
|
+
"ActionController::MissingExactTemplate" => :not_acceptable,
|
17
|
+
"ActionController::InvalidAuthenticityToken" => :unprocessable_entity,
|
18
|
+
"ActionController::InvalidCrossOriginRequest" => :unprocessable_entity,
|
19
|
+
"ActionDispatch::Http::Parameters::ParseError" => :bad_request,
|
20
|
+
"ActionController::BadRequest" => :bad_request,
|
21
|
+
"ActionController::ParameterMissing" => :bad_request,
|
22
|
+
"Rack::QueryParser::ParameterTypeError" => :bad_request,
|
23
|
+
"Rack::QueryParser::InvalidParameterError" => :bad_request
|
22
24
|
)
|
23
25
|
|
24
26
|
cattr_accessor :rescue_templates, default: Hash.new("diagnostics").merge!(
|
25
|
-
"ActionView::MissingTemplate"
|
26
|
-
"ActionController::RoutingError"
|
27
|
-
"AbstractController::ActionNotFound"
|
28
|
-
"ActiveRecord::StatementInvalid"
|
29
|
-
"ActionView::Template::Error"
|
27
|
+
"ActionView::MissingTemplate" => "missing_template",
|
28
|
+
"ActionController::RoutingError" => "routing_error",
|
29
|
+
"AbstractController::ActionNotFound" => "unknown_action",
|
30
|
+
"ActiveRecord::StatementInvalid" => "invalid_statement",
|
31
|
+
"ActionView::Template::Error" => "template_error",
|
32
|
+
"ActionController::MissingExactTemplate" => "missing_exact_template",
|
30
33
|
)
|
31
34
|
|
32
|
-
|
35
|
+
cattr_accessor :wrapper_exceptions, default: [
|
36
|
+
"ActionView::Template::Error"
|
37
|
+
]
|
38
|
+
|
39
|
+
cattr_accessor :silent_exceptions, default: [
|
40
|
+
"ActionController::RoutingError",
|
41
|
+
"ActionDispatch::Http::MimeNegotiation::InvalidType"
|
42
|
+
]
|
43
|
+
|
44
|
+
attr_reader :backtrace_cleaner, :exception, :wrapped_causes, :line_number, :file
|
33
45
|
|
34
46
|
def initialize(backtrace_cleaner, exception)
|
35
47
|
@backtrace_cleaner = backtrace_cleaner
|
36
|
-
@exception =
|
48
|
+
@exception = exception
|
49
|
+
@exception_class_name = @exception.class.name
|
50
|
+
@wrapped_causes = wrapped_causes_for(exception, backtrace_cleaner)
|
37
51
|
|
38
52
|
expand_backtrace if exception.is_a?(SyntaxError) || exception.cause.is_a?(SyntaxError)
|
39
53
|
end
|
40
54
|
|
55
|
+
def unwrapped_exception
|
56
|
+
if wrapper_exceptions.include?(@exception_class_name)
|
57
|
+
exception.cause
|
58
|
+
else
|
59
|
+
exception
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
41
63
|
def rescue_template
|
42
|
-
@@rescue_templates[@
|
64
|
+
@@rescue_templates[@exception_class_name]
|
43
65
|
end
|
44
66
|
|
45
67
|
def status_code
|
46
|
-
self.class.status_code_for_exception(
|
68
|
+
self.class.status_code_for_exception(unwrapped_exception.class.name)
|
69
|
+
end
|
70
|
+
|
71
|
+
def exception_trace
|
72
|
+
trace = application_trace
|
73
|
+
trace = framework_trace if trace.empty? && !silent_exceptions.include?(@exception_class_name)
|
74
|
+
trace
|
47
75
|
end
|
48
76
|
|
49
77
|
def application_trace
|
@@ -64,7 +92,11 @@ module ActionDispatch
|
|
64
92
|
full_trace_with_ids = []
|
65
93
|
|
66
94
|
full_trace.each_with_index do |trace, idx|
|
67
|
-
trace_with_id = {
|
95
|
+
trace_with_id = {
|
96
|
+
exception_object_id: @exception.object_id,
|
97
|
+
id: idx,
|
98
|
+
trace: trace
|
99
|
+
}
|
68
100
|
|
69
101
|
if application_trace.include?(trace)
|
70
102
|
application_trace_with_ids << trace_with_id
|
@@ -97,18 +129,31 @@ module ActionDispatch
|
|
97
129
|
end
|
98
130
|
end
|
99
131
|
|
100
|
-
|
132
|
+
def trace_to_show
|
133
|
+
if traces["Application Trace"].empty? && rescue_template != "routing_error"
|
134
|
+
"Full Trace"
|
135
|
+
else
|
136
|
+
"Application Trace"
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def source_to_show_id
|
141
|
+
(traces[trace_to_show].first || {})[:id]
|
142
|
+
end
|
101
143
|
|
144
|
+
private
|
102
145
|
def backtrace
|
103
146
|
Array(@exception.backtrace)
|
104
147
|
end
|
105
148
|
|
106
|
-
def
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
149
|
+
def causes_for(exception)
|
150
|
+
return enum_for(__method__, exception) unless block_given?
|
151
|
+
|
152
|
+
yield exception while exception = exception.cause
|
153
|
+
end
|
154
|
+
|
155
|
+
def wrapped_causes_for(exception, backtrace_cleaner)
|
156
|
+
causes_for(exception).map { |cause| self.class.new(backtrace_cleaner, cause) }
|
112
157
|
end
|
113
158
|
|
114
159
|
def clean_backtrace(*args)
|
@@ -38,7 +38,7 @@ module ActionDispatch
|
|
38
38
|
#
|
39
39
|
# See docs on the FlashHash class for more details about the flash.
|
40
40
|
class Flash
|
41
|
-
KEY = "action_dispatch.request.flash_hash"
|
41
|
+
KEY = "action_dispatch.request.flash_hash"
|
42
42
|
|
43
43
|
module RequestMethods
|
44
44
|
# Access the contents of the flash. Use <tt>flash["notice"]</tt> to
|
@@ -0,0 +1,170 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "action_dispatch/http/request"
|
4
|
+
|
5
|
+
module ActionDispatch
|
6
|
+
# This middleware guards from DNS rebinding attacks by explicitly permitting
|
7
|
+
# the hosts a request can be sent to, and is passed the options set in
|
8
|
+
# +config.host_authorization+.
|
9
|
+
#
|
10
|
+
# Requests can opt-out of Host Authorization with +exclude+:
|
11
|
+
#
|
12
|
+
# config.host_authorization = { exclude: ->(request) { request.path =~ /healthcheck/ } }
|
13
|
+
#
|
14
|
+
# When a request comes to an unauthorized host, the +response_app+
|
15
|
+
# application will be executed and rendered. If no +response_app+ is given, a
|
16
|
+
# default one will run.
|
17
|
+
# The default response app logs blocked host info with level 'error' and
|
18
|
+
# responds with <tt>403 Forbidden</tt>. The body of the response contains debug info
|
19
|
+
# if +config.consider_all_requests_local+ is set to true, otherwise the body is empty.
|
20
|
+
class HostAuthorization
|
21
|
+
ALLOWED_HOSTS_IN_DEVELOPMENT = [".localhost", IPAddr.new("0.0.0.0/0"), IPAddr.new("::/0")]
|
22
|
+
PORT_REGEX = /(?::\d+)/ # :nodoc:
|
23
|
+
IPV4_HOSTNAME = /(?<host>\d+\.\d+\.\d+\.\d+)#{PORT_REGEX}?/ # :nodoc:
|
24
|
+
IPV6_HOSTNAME = /(?<host>[a-f0-9]*:[a-f0-9.:]+)/i # :nodoc:
|
25
|
+
IPV6_HOSTNAME_WITH_PORT = /\[#{IPV6_HOSTNAME}\]#{PORT_REGEX}/i # :nodoc:
|
26
|
+
VALID_IP_HOSTNAME = Regexp.union( # :nodoc:
|
27
|
+
/\A#{IPV4_HOSTNAME}\z/,
|
28
|
+
/\A#{IPV6_HOSTNAME}\z/,
|
29
|
+
/\A#{IPV6_HOSTNAME_WITH_PORT}\z/,
|
30
|
+
)
|
31
|
+
|
32
|
+
class Permissions # :nodoc:
|
33
|
+
def initialize(hosts)
|
34
|
+
@hosts = sanitize_hosts(hosts)
|
35
|
+
end
|
36
|
+
|
37
|
+
def empty?
|
38
|
+
@hosts.empty?
|
39
|
+
end
|
40
|
+
|
41
|
+
def allows?(host)
|
42
|
+
@hosts.any? do |allowed|
|
43
|
+
if allowed.is_a?(IPAddr)
|
44
|
+
begin
|
45
|
+
allowed === extract_hostname(host)
|
46
|
+
rescue
|
47
|
+
# IPAddr#=== raises an error if you give it a hostname instead of
|
48
|
+
# IP. Treat similar errors as blocked access.
|
49
|
+
false
|
50
|
+
end
|
51
|
+
else
|
52
|
+
allowed === host
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
def sanitize_hosts(hosts)
|
59
|
+
Array(hosts).map do |host|
|
60
|
+
case host
|
61
|
+
when Regexp then sanitize_regexp(host)
|
62
|
+
when String then sanitize_string(host)
|
63
|
+
else host
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def sanitize_regexp(host)
|
69
|
+
/\A#{host}#{PORT_REGEX}?\z/
|
70
|
+
end
|
71
|
+
|
72
|
+
def sanitize_string(host)
|
73
|
+
if host.start_with?(".")
|
74
|
+
/\A([a-z0-9-]+\.)?#{Regexp.escape(host[1..-1])}#{PORT_REGEX}?\z/i
|
75
|
+
else
|
76
|
+
/\A#{Regexp.escape host}#{PORT_REGEX}?\z/i
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def extract_hostname(host)
|
81
|
+
host.slice(VALID_IP_HOSTNAME, "host") || host
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
class DefaultResponseApp # :nodoc:
|
86
|
+
RESPONSE_STATUS = 403
|
87
|
+
|
88
|
+
def call(env)
|
89
|
+
request = Request.new(env)
|
90
|
+
format = request.xhr? ? "text/plain" : "text/html"
|
91
|
+
|
92
|
+
log_error(request)
|
93
|
+
response(format, response_body(request))
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
def response_body(request)
|
98
|
+
return "" unless request.get_header("action_dispatch.show_detailed_exceptions")
|
99
|
+
|
100
|
+
template = DebugView.new(host: request.host)
|
101
|
+
template.render(template: "rescues/blocked_host", layout: "rescues/layout")
|
102
|
+
end
|
103
|
+
|
104
|
+
def response(format, body)
|
105
|
+
[RESPONSE_STATUS,
|
106
|
+
{ "Content-Type" => "#{format}; charset=#{Response.default_charset}",
|
107
|
+
"Content-Length" => body.bytesize.to_s },
|
108
|
+
[body]]
|
109
|
+
end
|
110
|
+
|
111
|
+
def log_error(request)
|
112
|
+
logger = available_logger(request)
|
113
|
+
|
114
|
+
return unless logger
|
115
|
+
|
116
|
+
logger.error("[#{self.class.name}] Blocked host: #{request.host}")
|
117
|
+
end
|
118
|
+
|
119
|
+
def available_logger(request)
|
120
|
+
request.logger || ActionView::Base.logger
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def initialize(app, hosts, deprecated_response_app = nil, exclude: nil, response_app: nil)
|
125
|
+
@app = app
|
126
|
+
@permissions = Permissions.new(hosts)
|
127
|
+
@exclude = exclude
|
128
|
+
|
129
|
+
unless deprecated_response_app.nil?
|
130
|
+
ActiveSupport::Deprecation.warn(<<-MSG.squish)
|
131
|
+
`action_dispatch.hosts_response_app` is deprecated and will be ignored in Rails 7.0.
|
132
|
+
Use the Host Authorization `response_app` setting instead.
|
133
|
+
MSG
|
134
|
+
|
135
|
+
response_app ||= deprecated_response_app
|
136
|
+
end
|
137
|
+
|
138
|
+
@response_app = response_app || DefaultResponseApp.new
|
139
|
+
end
|
140
|
+
|
141
|
+
def call(env)
|
142
|
+
return @app.call(env) if @permissions.empty?
|
143
|
+
|
144
|
+
request = Request.new(env)
|
145
|
+
|
146
|
+
if authorized?(request) || excluded?(request)
|
147
|
+
mark_as_authorized(request)
|
148
|
+
@app.call(env)
|
149
|
+
else
|
150
|
+
@response_app.call(env)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
private
|
155
|
+
def authorized?(request)
|
156
|
+
origin_host = request.get_header("HTTP_HOST")
|
157
|
+
forwarded_host = request.x_forwarded_host&.split(/,\s?/)&.last
|
158
|
+
|
159
|
+
@permissions.allows?(origin_host) && (forwarded_host.blank? || @permissions.allows?(forwarded_host))
|
160
|
+
end
|
161
|
+
|
162
|
+
def excluded?(request)
|
163
|
+
@exclude && @exclude.call(request)
|
164
|
+
end
|
165
|
+
|
166
|
+
def mark_as_authorized(request)
|
167
|
+
request.set_header("action_dispatch.authorized_host", request.host)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
@@ -21,14 +21,17 @@ module ActionDispatch
|
|
21
21
|
def call(env)
|
22
22
|
request = ActionDispatch::Request.new(env)
|
23
23
|
status = request.path_info[1..-1].to_i
|
24
|
-
|
25
|
-
|
24
|
+
begin
|
25
|
+
content_type = request.formats.first
|
26
|
+
rescue ActionDispatch::Http::MimeNegotiation::InvalidType
|
27
|
+
content_type = Mime[:text]
|
28
|
+
end
|
29
|
+
body = { status: status, error: Rack::Utils::HTTP_STATUS_CODES.fetch(status, Rack::Utils::HTTP_STATUS_CODES[500]) }
|
26
30
|
|
27
31
|
render(status, content_type, body)
|
28
32
|
end
|
29
33
|
|
30
34
|
private
|
31
|
-
|
32
35
|
def render(status, content_type, body)
|
33
36
|
format = "to_#{content_type.to_sym}" if content_type
|
34
37
|
if format && body.respond_to?(format)
|
@@ -8,13 +8,13 @@ module ActionDispatch
|
|
8
8
|
# contain the address, and then picking the last-set address that is not
|
9
9
|
# on the list of trusted IPs. This follows the precedent set by e.g.
|
10
10
|
# {the Tomcat server}[https://issues.apache.org/bugzilla/show_bug.cgi?id=50453],
|
11
|
-
# with {reasoning explained at length}[
|
11
|
+
# with {reasoning explained at length}[https://blog.gingerlime.com/2012/rails-ip-spoofing-vulnerabilities-and-protection]
|
12
12
|
# by @gingerlime. A more detailed explanation of the algorithm is given
|
13
13
|
# at GetIp#calculate_ip.
|
14
14
|
#
|
15
15
|
# Some Rack servers concatenate repeated headers, like {HTTP RFC 2616}[https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2]
|
16
16
|
# requires. Some Rack servers simply drop preceding headers, and only report
|
17
|
-
# the value that was {given in the last header}[
|
17
|
+
# the value that was {given in the last header}[https://andre.arko.net/2011/12/26/repeated-headers-and-ruby-web-servers].
|
18
18
|
# If you are behind multiple proxy servers (like NGINX to HAProxy to Unicorn)
|
19
19
|
# then you should test your Rack server to make sure your data is good.
|
20
20
|
#
|
@@ -33,7 +33,7 @@ module ActionDispatch
|
|
33
33
|
# not be the ultimate client IP in production, and so are discarded. See
|
34
34
|
# https://en.wikipedia.org/wiki/Private_network for details.
|
35
35
|
TRUSTED_PROXIES = [
|
36
|
-
"127.0.0.
|
36
|
+
"127.0.0.0/8", # localhost IPv4 range, per RFC-3330
|
37
37
|
"::1", # localhost IPv6
|
38
38
|
"fc00::/7", # private IPv6 range fc00::/7
|
39
39
|
"10.0.0.0/8", # private IPv4 range 10.x.x.x
|
@@ -102,7 +102,7 @@ module ActionDispatch
|
|
102
102
|
# proxies, that header may contain a list of IPs. Other proxy services
|
103
103
|
# set the Client-Ip header instead, so we check that too.
|
104
104
|
#
|
105
|
-
# As discussed in {this post about Rails IP Spoofing}[
|
105
|
+
# As discussed in {this post about Rails IP Spoofing}[https://blog.gingerlime.com/2012/rails-ip-spoofing-vulnerabilities-and-protection/],
|
106
106
|
# while the first IP in the list is likely to be the "originating" IP,
|
107
107
|
# it could also have been set by the client maliciously.
|
108
108
|
#
|
@@ -143,10 +143,11 @@ module ActionDispatch
|
|
143
143
|
# - X-Forwarded-For will be a list of IPs, one per proxy, or blank
|
144
144
|
# - Client-Ip is propagated from the outermost proxy, or is blank
|
145
145
|
# - REMOTE_ADDR will be the IP that made the request to Rack
|
146
|
-
ips = [forwarded_ips, client_ips
|
146
|
+
ips = [forwarded_ips, client_ips].flatten.compact
|
147
147
|
|
148
|
-
# If every single IP option is in the trusted list,
|
149
|
-
|
148
|
+
# If every single IP option is in the trusted list, return the IP
|
149
|
+
# that's furthest away
|
150
|
+
filter_proxies(ips + [remote_addr]).first || ips.last || remote_addr
|
150
151
|
end
|
151
152
|
|
152
153
|
# Memoizes the value returned by #calculate_ip and returns it for
|
@@ -156,20 +157,17 @@ module ActionDispatch
|
|
156
157
|
end
|
157
158
|
|
158
159
|
private
|
159
|
-
|
160
160
|
def ips_from(header) # :doc:
|
161
161
|
return [] unless header
|
162
162
|
# Split the comma-separated list into an array of strings.
|
163
163
|
ips = header.strip.split(/[,\s]+/)
|
164
164
|
ips.select do |ip|
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
nil
|
172
|
-
end
|
165
|
+
# Only return IPs that are valid according to the IPAddr#new method.
|
166
|
+
range = IPAddr.new(ip).to_range
|
167
|
+
# We want to make sure nobody is sneaking a netmask in.
|
168
|
+
range.begin == range.end
|
169
|
+
rescue ArgumentError
|
170
|
+
nil
|
173
171
|
end
|
174
172
|
end
|
175
173
|
|
@@ -15,22 +15,21 @@ module ActionDispatch
|
|
15
15
|
# The unique request id can be used to trace a request end-to-end and would typically end up being part of log files
|
16
16
|
# from multiple pieces of the stack.
|
17
17
|
class RequestId
|
18
|
-
|
19
|
-
|
20
|
-
def initialize(app)
|
18
|
+
def initialize(app, header:)
|
21
19
|
@app = app
|
20
|
+
@header = header
|
22
21
|
end
|
23
22
|
|
24
23
|
def call(env)
|
25
24
|
req = ActionDispatch::Request.new env
|
26
|
-
req.request_id = make_request_id(req.
|
27
|
-
@app.call(env).tap { |_status, headers, _body| headers[
|
25
|
+
req.request_id = make_request_id(req.headers[@header])
|
26
|
+
@app.call(env).tap { |_status, headers, _body| headers[@header] = req.request_id }
|
28
27
|
end
|
29
28
|
|
30
29
|
private
|
31
30
|
def make_request_id(request_id)
|
32
31
|
if request_id.presence
|
33
|
-
request_id.gsub(/[^\w\-@]/, ""
|
32
|
+
request_id.gsub(/[^\w\-@]/, "").first(255)
|
34
33
|
else
|
35
34
|
internal_request_id
|
36
35
|
end
|
@@ -30,7 +30,6 @@ module ActionDispatch
|
|
30
30
|
end
|
31
31
|
|
32
32
|
private
|
33
|
-
|
34
33
|
def initialize_sid # :doc:
|
35
34
|
@default_options.delete(:sidbits)
|
36
35
|
@default_options.delete(:secure_random)
|
@@ -83,7 +82,7 @@ module ActionDispatch
|
|
83
82
|
include SessionObject
|
84
83
|
|
85
84
|
private
|
86
|
-
def set_cookie(request,
|
85
|
+
def set_cookie(request, response, cookie)
|
87
86
|
request.cookie_jar[key] = cookie
|
88
87
|
end
|
89
88
|
end
|
@@ -98,7 +97,7 @@ module ActionDispatch
|
|
98
97
|
end
|
99
98
|
|
100
99
|
private
|
101
|
-
def set_cookie(request,
|
100
|
+
def set_cookie(request, response, cookie)
|
102
101
|
request.cookie_jar[key] = cookie
|
103
102
|
end
|
104
103
|
end
|
@@ -10,22 +10,17 @@ module ActionDispatch
|
|
10
10
|
# dramatically faster than the alternatives.
|
11
11
|
#
|
12
12
|
# Sessions typically contain at most a user_id and flash message; both fit
|
13
|
-
# within the
|
14
|
-
# you attempt to store more than
|
13
|
+
# within the 4096 bytes cookie size limit. A CookieOverflow exception is raised if
|
14
|
+
# you attempt to store more than 4096 bytes of data.
|
15
15
|
#
|
16
16
|
# The cookie jar used for storage is automatically configured to be the
|
17
17
|
# best possible option given your application's configuration.
|
18
18
|
#
|
19
|
-
# If you only have secret_token set, your cookies will be signed, but
|
20
|
-
# not encrypted. This means a user cannot alter their +user_id+ without
|
21
|
-
# knowing your app's secret key, but can easily read their +user_id+. This
|
22
|
-
# was the default for Rails 3 apps.
|
23
|
-
#
|
24
19
|
# Your cookies will be encrypted using your apps secret_key_base. This
|
25
20
|
# goes a step further than signed cookies in that encrypted cookies cannot
|
26
21
|
# be altered or read by users. This is the default starting in Rails 4.
|
27
22
|
#
|
28
|
-
# Configure your session store in
|
23
|
+
# Configure your session store in an initializer:
|
29
24
|
#
|
30
25
|
# Rails.application.config.session_store :cookie_store, key: '_your_app_session'
|
31
26
|
#
|
@@ -81,7 +76,6 @@ module ActionDispatch
|
|
81
76
|
end
|
82
77
|
|
83
78
|
private
|
84
|
-
|
85
79
|
def extract_session_id(req)
|
86
80
|
stale_session_check! do
|
87
81
|
sid = unpacked_cookie_data(req)["session_id"]
|
@@ -40,14 +40,16 @@ module ActionDispatch
|
|
40
40
|
end
|
41
41
|
|
42
42
|
private
|
43
|
-
|
44
43
|
def render_exception(request, exception)
|
45
44
|
backtrace_cleaner = request.get_header "action_dispatch.backtrace_cleaner"
|
46
45
|
wrapper = ExceptionWrapper.new(backtrace_cleaner, exception)
|
47
46
|
status = wrapper.status_code
|
48
|
-
request.set_header "action_dispatch.exception", wrapper.
|
47
|
+
request.set_header "action_dispatch.exception", wrapper.unwrapped_exception
|
49
48
|
request.set_header "action_dispatch.original_path", request.path_info
|
49
|
+
request.set_header "action_dispatch.original_request_method", request.raw_request_method
|
50
|
+
fallback_to_html_format_if_invalid_mime_type(request)
|
50
51
|
request.path_info = "/#{status}"
|
52
|
+
request.request_method = "GET"
|
51
53
|
response = @exceptions_app.call(request.env)
|
52
54
|
response[1]["X-Cascade"] == "pass" ? pass_response(status) : response
|
53
55
|
rescue Exception => failsafe_error
|
@@ -55,6 +57,15 @@ module ActionDispatch
|
|
55
57
|
FAILSAFE_RESPONSE
|
56
58
|
end
|
57
59
|
|
60
|
+
def fallback_to_html_format_if_invalid_mime_type(request)
|
61
|
+
# If the MIME type for the request is invalid then the
|
62
|
+
# @exceptions_app may not be able to handle it. To make it
|
63
|
+
# easier to handle, we switch to HTML.
|
64
|
+
request.formats
|
65
|
+
rescue ActionDispatch::Http::MimeNegotiation::InvalidType
|
66
|
+
request.set_header "HTTP_ACCEPT", "text/html"
|
67
|
+
end
|
68
|
+
|
58
69
|
def pass_response(status)
|
59
70
|
[status, { "Content-Type" => "text/html; charset=#{Response.default_charset}", "Content-Length" => "0" }, []]
|
60
71
|
end
|