actionpack 5.2.1 → 7.0.2.4
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.
Potentially problematic release.
This version of actionpack might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +264 -220
- data/MIT-LICENSE +1 -1
- data/README.rdoc +6 -6
- data/lib/abstract_controller/asset_paths.rb +1 -1
- data/lib/abstract_controller/base.rb +24 -4
- data/lib/abstract_controller/caching/fragments.rb +8 -24
- data/lib/abstract_controller/caching.rb +2 -2
- data/lib/abstract_controller/callbacks.rb +34 -8
- data/lib/abstract_controller/collector.rb +5 -4
- data/lib/abstract_controller/error.rb +1 -1
- data/lib/abstract_controller/helpers.rb +107 -90
- data/lib/abstract_controller/logger.rb +1 -1
- data/lib/abstract_controller/railties/routes_helpers.rb +19 -1
- data/lib/abstract_controller/rendering.rb +9 -9
- data/lib/abstract_controller/translation.rb +12 -5
- data/lib/abstract_controller/url_for.rb +4 -6
- data/lib/abstract_controller.rb +2 -0
- data/lib/action_controller/api.rb +5 -4
- data/lib/action_controller/base.rb +6 -9
- data/lib/action_controller/caching.rb +1 -3
- data/lib/action_controller/log_subscriber.rb +13 -9
- data/lib/action_controller/metal/basic_implicit_render.rb +1 -1
- data/lib/action_controller/metal/conditional_get.rb +57 -6
- data/lib/action_controller/metal/content_security_policy.rb +2 -3
- data/lib/action_controller/metal/cookies.rb +4 -2
- data/lib/action_controller/metal/data_streaming.rb +9 -18
- 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 +55 -12
- data/lib/action_controller/metal/flash.rb +10 -6
- data/lib/action_controller/metal/head.rb +7 -4
- data/lib/action_controller/metal/helpers.rb +15 -6
- data/lib/action_controller/metal/http_authentication.rb +41 -39
- data/lib/action_controller/metal/implicit_render.rb +5 -15
- data/lib/action_controller/metal/instrumentation.rb +59 -55
- data/lib/action_controller/metal/live.rb +80 -33
- data/lib/action_controller/metal/logging.rb +20 -0
- data/lib/action_controller/metal/mime_responds.rb +22 -7
- data/lib/action_controller/metal/parameter_encoding.rb +35 -4
- data/lib/action_controller/metal/params_wrapper.rb +50 -31
- data/lib/action_controller/metal/permissions_policy.rb +46 -0
- data/lib/action_controller/metal/redirecting.rb +93 -23
- data/lib/action_controller/metal/renderers.rb +4 -4
- data/lib/action_controller/metal/rendering.rb +14 -9
- data/lib/action_controller/metal/request_forgery_protection.rb +160 -58
- data/lib/action_controller/metal/rescue.rb +2 -2
- data/lib/action_controller/metal/streaming.rb +1 -4
- data/lib/action_controller/metal/strong_parameters.rb +236 -88
- data/lib/action_controller/metal/testing.rb +9 -2
- data/lib/action_controller/metal/url_for.rb +1 -1
- data/lib/action_controller/metal.rb +16 -17
- data/lib/action_controller/railtie.rb +49 -6
- 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 +98 -68
- data/lib/action_controller.rb +4 -5
- data/lib/action_dispatch/http/cache.rb +45 -32
- data/lib/action_dispatch/http/content_disposition.rb +45 -0
- data/lib/action_dispatch/http/content_security_policy.rb +69 -56
- data/lib/action_dispatch/http/filter_parameters.rb +14 -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 +44 -16
- data/lib/action_dispatch/http/mime_type.rb +47 -30
- data/lib/action_dispatch/http/parameters.rb +18 -27
- data/lib/action_dispatch/http/permissions_policy.rb +173 -0
- data/lib/action_dispatch/http/request.rb +49 -35
- data/lib/action_dispatch/http/response.rb +34 -26
- data/lib/action_dispatch/http/upload.rb +9 -1
- data/lib/action_dispatch/http/url.rb +86 -94
- data/lib/action_dispatch/journey/formatter.rb +55 -31
- data/lib/action_dispatch/journey/gtg/builder.rb +30 -46
- data/lib/action_dispatch/journey/gtg/simulator.rb +15 -8
- data/lib/action_dispatch/journey/gtg/transition_table.rb +78 -21
- data/lib/action_dispatch/journey/nfa/dot.rb +0 -11
- data/lib/action_dispatch/journey/nodes/node.rb +83 -16
- 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 +42 -34
- data/lib/action_dispatch/journey/route.rb +14 -31
- data/lib/action_dispatch/journey/router/utils.rb +16 -14
- data/lib/action_dispatch/journey/router.rb +27 -35
- data/lib/action_dispatch/journey/routes.rb +3 -5
- data/lib/action_dispatch/journey/scanner.rb +10 -4
- data/lib/action_dispatch/journey/visitors.rb +1 -4
- data/lib/action_dispatch/journey/visualizer/fsm.js +49 -24
- data/lib/action_dispatch/journey/visualizer/index.html.erb +1 -1
- data/lib/action_dispatch/journey.rb +0 -2
- data/lib/action_dispatch/middleware/actionable_exceptions.rb +45 -0
- data/lib/action_dispatch/middleware/callbacks.rb +2 -4
- data/lib/action_dispatch/middleware/cookies.rb +136 -113
- data/lib/action_dispatch/middleware/debug_exceptions.rb +47 -68
- data/lib/action_dispatch/middleware/debug_locks.rb +8 -8
- data/lib/action_dispatch/middleware/debug_view.rb +66 -0
- data/lib/action_dispatch/middleware/exception_wrapper.rb +79 -30
- data/lib/action_dispatch/middleware/executor.rb +4 -1
- data/lib/action_dispatch/middleware/flash.rb +10 -12
- data/lib/action_dispatch/middleware/host_authorization.rb +159 -0
- data/lib/action_dispatch/middleware/public_exceptions.rb +6 -3
- data/lib/action_dispatch/middleware/remote_ip.rb +30 -20
- data/lib/action_dispatch/middleware/request_id.rb +5 -6
- data/lib/action_dispatch/middleware/server_timing.rb +33 -0
- data/lib/action_dispatch/middleware/session/abstract_store.rb +16 -3
- data/lib/action_dispatch/middleware/session/cache_store.rb +11 -6
- data/lib/action_dispatch/middleware/session/cookie_store.rb +24 -19
- data/lib/action_dispatch/middleware/show_exceptions.rb +20 -11
- data/lib/action_dispatch/middleware/ssl.rb +20 -15
- data/lib/action_dispatch/middleware/stack.rb +79 -7
- data/lib/action_dispatch/middleware/static.rb +150 -94
- 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 +6 -11
- 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 +46 -36
- data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +8 -0
- data/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +7 -0
- data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +25 -6
- data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +1 -1
- data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +9 -6
- data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +4 -1
- data/lib/action_dispatch/middleware/templates/rescues/layout.erb +121 -15
- 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 +5 -5
- data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +4 -4
- data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +5 -5
- data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +4 -4
- data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +16 -2
- data/lib/action_dispatch/railtie.rb +16 -4
- data/lib/action_dispatch/request/session.rb +59 -22
- data/lib/action_dispatch/request/utils.rb +28 -2
- data/lib/action_dispatch/routing/inspector.rb +102 -54
- data/lib/action_dispatch/routing/mapper.rb +184 -156
- data/lib/action_dispatch/routing/polymorphic_routes.rb +21 -19
- data/lib/action_dispatch/routing/redirection.rb +4 -6
- data/lib/action_dispatch/routing/route_set.rb +83 -73
- data/lib/action_dispatch/routing/routes_proxy.rb +1 -1
- data/lib/action_dispatch/routing/url_for.rb +2 -3
- data/lib/action_dispatch/routing.rb +23 -22
- data/lib/action_dispatch/system_test_case.rb +65 -16
- data/lib/action_dispatch/system_testing/browser.rb +43 -16
- data/lib/action_dispatch/system_testing/driver.rb +42 -10
- data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +58 -12
- data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +3 -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 +3 -6
- data/lib/action_dispatch/testing/integration.rb +61 -30
- data/lib/action_dispatch/testing/request_encoder.rb +2 -2
- data/lib/action_dispatch/testing/test_process.rb +8 -6
- 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 +15 -7
- data/lib/action_pack/gem_version.rb +4 -4
- data/lib/action_pack.rb +1 -1
- metadata +44 -25
- 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,159 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionDispatch
|
4
|
+
# This middleware guards from DNS rebinding attacks by explicitly permitting
|
5
|
+
# the hosts a request can be sent to, and is passed the options set in
|
6
|
+
# +config.host_authorization+.
|
7
|
+
#
|
8
|
+
# Requests can opt-out of Host Authorization with +exclude+:
|
9
|
+
#
|
10
|
+
# config.host_authorization = { exclude: ->(request) { request.path =~ /healthcheck/ } }
|
11
|
+
#
|
12
|
+
# When a request comes to an unauthorized host, the +response_app+
|
13
|
+
# application will be executed and rendered. If no +response_app+ is given, a
|
14
|
+
# default one will run.
|
15
|
+
# The default response app logs blocked host info with level 'error' and
|
16
|
+
# responds with <tt>403 Forbidden</tt>. The body of the response contains debug info
|
17
|
+
# if +config.consider_all_requests_local+ is set to true, otherwise the body is empty.
|
18
|
+
class HostAuthorization
|
19
|
+
ALLOWED_HOSTS_IN_DEVELOPMENT = [".localhost", IPAddr.new("0.0.0.0/0"), IPAddr.new("::/0")]
|
20
|
+
PORT_REGEX = /(?::\d+)/ # :nodoc:
|
21
|
+
IPV4_HOSTNAME = /(?<host>\d+\.\d+\.\d+\.\d+)#{PORT_REGEX}?/ # :nodoc:
|
22
|
+
IPV6_HOSTNAME = /(?<host>[a-f0-9]*:[a-f0-9.:]+)/i # :nodoc:
|
23
|
+
IPV6_HOSTNAME_WITH_PORT = /\[#{IPV6_HOSTNAME}\]#{PORT_REGEX}/i # :nodoc:
|
24
|
+
VALID_IP_HOSTNAME = Regexp.union( # :nodoc:
|
25
|
+
/\A#{IPV4_HOSTNAME}\z/,
|
26
|
+
/\A#{IPV6_HOSTNAME}\z/,
|
27
|
+
/\A#{IPV6_HOSTNAME_WITH_PORT}\z/,
|
28
|
+
)
|
29
|
+
|
30
|
+
class Permissions # :nodoc:
|
31
|
+
def initialize(hosts)
|
32
|
+
@hosts = sanitize_hosts(hosts)
|
33
|
+
end
|
34
|
+
|
35
|
+
def empty?
|
36
|
+
@hosts.empty?
|
37
|
+
end
|
38
|
+
|
39
|
+
def allows?(host)
|
40
|
+
@hosts.any? do |allowed|
|
41
|
+
if allowed.is_a?(IPAddr)
|
42
|
+
begin
|
43
|
+
allowed === extract_hostname(host)
|
44
|
+
rescue
|
45
|
+
# IPAddr#=== raises an error if you give it a hostname instead of
|
46
|
+
# IP. Treat similar errors as blocked access.
|
47
|
+
false
|
48
|
+
end
|
49
|
+
else
|
50
|
+
allowed === host
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
def sanitize_hosts(hosts)
|
57
|
+
Array(hosts).map do |host|
|
58
|
+
case host
|
59
|
+
when Regexp then sanitize_regexp(host)
|
60
|
+
when String then sanitize_string(host)
|
61
|
+
else host
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def sanitize_regexp(host)
|
67
|
+
/\A#{host}#{PORT_REGEX}?\z/
|
68
|
+
end
|
69
|
+
|
70
|
+
def sanitize_string(host)
|
71
|
+
if host.start_with?(".")
|
72
|
+
/\A([a-z0-9-]+\.)?#{Regexp.escape(host[1..-1])}#{PORT_REGEX}?\z/i
|
73
|
+
else
|
74
|
+
/\A#{Regexp.escape host}#{PORT_REGEX}?\z/i
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def extract_hostname(host)
|
79
|
+
host.slice(VALID_IP_HOSTNAME, "host") || host
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
class DefaultResponseApp # :nodoc:
|
84
|
+
RESPONSE_STATUS = 403
|
85
|
+
|
86
|
+
def call(env)
|
87
|
+
request = Request.new(env)
|
88
|
+
format = request.xhr? ? "text/plain" : "text/html"
|
89
|
+
|
90
|
+
log_error(request)
|
91
|
+
response(format, response_body(request))
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
def response_body(request)
|
96
|
+
return "" unless request.get_header("action_dispatch.show_detailed_exceptions")
|
97
|
+
|
98
|
+
template = DebugView.new(host: request.host)
|
99
|
+
template.render(template: "rescues/blocked_host", layout: "rescues/layout")
|
100
|
+
end
|
101
|
+
|
102
|
+
def response(format, body)
|
103
|
+
[RESPONSE_STATUS,
|
104
|
+
{ "Content-Type" => "#{format}; charset=#{Response.default_charset}",
|
105
|
+
"Content-Length" => body.bytesize.to_s },
|
106
|
+
[body]]
|
107
|
+
end
|
108
|
+
|
109
|
+
def log_error(request)
|
110
|
+
logger = available_logger(request)
|
111
|
+
|
112
|
+
return unless logger
|
113
|
+
|
114
|
+
logger.error("[#{self.class.name}] Blocked host: #{request.host}")
|
115
|
+
end
|
116
|
+
|
117
|
+
def available_logger(request)
|
118
|
+
request.logger || ActionView::Base.logger
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def initialize(app, hosts, exclude: nil, response_app: nil)
|
123
|
+
@app = app
|
124
|
+
@permissions = Permissions.new(hosts)
|
125
|
+
@exclude = exclude
|
126
|
+
|
127
|
+
@response_app = response_app || DefaultResponseApp.new
|
128
|
+
end
|
129
|
+
|
130
|
+
def call(env)
|
131
|
+
return @app.call(env) if @permissions.empty?
|
132
|
+
|
133
|
+
request = Request.new(env)
|
134
|
+
|
135
|
+
if authorized?(request) || excluded?(request)
|
136
|
+
mark_as_authorized(request)
|
137
|
+
@app.call(env)
|
138
|
+
else
|
139
|
+
@response_app.call(env)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
private
|
144
|
+
def authorized?(request)
|
145
|
+
origin_host = request.get_header("HTTP_HOST")
|
146
|
+
forwarded_host = request.x_forwarded_host&.split(/,\s?/)&.last
|
147
|
+
|
148
|
+
@permissions.allows?(origin_host) && (forwarded_host.blank? || @permissions.allows?(forwarded_host))
|
149
|
+
end
|
150
|
+
|
151
|
+
def excluded?(request)
|
152
|
+
@exclude && @exclude.call(request)
|
153
|
+
end
|
154
|
+
|
155
|
+
def mark_as_authorized(request)
|
156
|
+
request.set_header("action_dispatch.authorized_host", request.host)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
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
|
@@ -51,10 +51,8 @@ module ActionDispatch
|
|
51
51
|
# clients (like WAP devices), or behind proxies that set headers in an
|
52
52
|
# incorrect or confusing way (like AWS ELB).
|
53
53
|
#
|
54
|
-
# The +custom_proxies+ argument can take an
|
55
|
-
#
|
56
|
-
# single string, IPAddr, or Regexp object is provided, it will be used in
|
57
|
-
# addition to +TRUSTED_PROXIES+. Any proxy setup will put the value you
|
54
|
+
# The +custom_proxies+ argument can take an enumerable which will be used
|
55
|
+
# instead of +TRUSTED_PROXIES+. Any proxy setup will put the value you
|
58
56
|
# want in the middle (or at the beginning) of the X-Forwarded-For list,
|
59
57
|
# with your proxy servers after it. If your proxies aren't removed, pass
|
60
58
|
# them in via the +custom_proxies+ parameter. That way, the middleware will
|
@@ -67,6 +65,20 @@ module ActionDispatch
|
|
67
65
|
elsif custom_proxies.respond_to?(:any?)
|
68
66
|
custom_proxies
|
69
67
|
else
|
68
|
+
ActiveSupport::Deprecation.warn(<<~EOM)
|
69
|
+
Setting config.action_dispatch.trusted_proxies to a single value has
|
70
|
+
been deprecated. Please set this to an enumerable instead. For
|
71
|
+
example, instead of:
|
72
|
+
|
73
|
+
config.action_dispatch.trusted_proxies = IPAddr.new("10.0.0.0/8")
|
74
|
+
|
75
|
+
Wrap the value in an Array:
|
76
|
+
|
77
|
+
config.action_dispatch.trusted_proxies = [IPAddr.new("10.0.0.0/8")]
|
78
|
+
|
79
|
+
Note that unlike passing a single argument, passing an enumerable
|
80
|
+
will *replace* the default set of trusted proxies.
|
81
|
+
EOM
|
70
82
|
Array(custom_proxies) + TRUSTED_PROXIES
|
71
83
|
end
|
72
84
|
end
|
@@ -102,7 +114,7 @@ module ActionDispatch
|
|
102
114
|
# proxies, that header may contain a list of IPs. Other proxy services
|
103
115
|
# set the Client-Ip header instead, so we check that too.
|
104
116
|
#
|
105
|
-
# As discussed in {this post about Rails IP Spoofing}[
|
117
|
+
# As discussed in {this post about Rails IP Spoofing}[https://blog.gingerlime.com/2012/rails-ip-spoofing-vulnerabilities-and-protection/],
|
106
118
|
# while the first IP in the list is likely to be the "originating" IP,
|
107
119
|
# it could also have been set by the client maliciously.
|
108
120
|
#
|
@@ -143,10 +155,11 @@ module ActionDispatch
|
|
143
155
|
# - X-Forwarded-For will be a list of IPs, one per proxy, or blank
|
144
156
|
# - Client-Ip is propagated from the outermost proxy, or is blank
|
145
157
|
# - REMOTE_ADDR will be the IP that made the request to Rack
|
146
|
-
ips = [forwarded_ips, client_ips
|
158
|
+
ips = [forwarded_ips, client_ips].flatten.compact
|
147
159
|
|
148
|
-
# If every single IP option is in the trusted list,
|
149
|
-
|
160
|
+
# If every single IP option is in the trusted list, return the IP
|
161
|
+
# that's furthest away
|
162
|
+
filter_proxies(ips + [remote_addr]).first || ips.last || remote_addr
|
150
163
|
end
|
151
164
|
|
152
165
|
# Memoizes the value returned by #calculate_ip and returns it for
|
@@ -156,20 +169,17 @@ module ActionDispatch
|
|
156
169
|
end
|
157
170
|
|
158
171
|
private
|
159
|
-
|
160
172
|
def ips_from(header) # :doc:
|
161
173
|
return [] unless header
|
162
174
|
# Split the comma-separated list into an array of strings.
|
163
175
|
ips = header.strip.split(/[,\s]+/)
|
164
176
|
ips.select do |ip|
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
nil
|
172
|
-
end
|
177
|
+
# Only return IPs that are valid according to the IPAddr#new method.
|
178
|
+
range = IPAddr.new(ip).to_range
|
179
|
+
# We want to make sure nobody is sneaking a netmask in.
|
180
|
+
range.begin == range.end
|
181
|
+
rescue ArgumentError
|
182
|
+
nil
|
173
183
|
end
|
174
184
|
end
|
175
185
|
|
@@ -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
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/notifications"
|
4
|
+
|
5
|
+
module ActionDispatch
|
6
|
+
class ServerTiming
|
7
|
+
SERVER_TIMING_HEADER = "Server-Timing"
|
8
|
+
|
9
|
+
def initialize(app)
|
10
|
+
@app = app
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(env)
|
14
|
+
events = []
|
15
|
+
subscriber = ActiveSupport::Notifications.subscribe(/.*/) do |*args|
|
16
|
+
events << ActiveSupport::Notifications::Event.new(*args)
|
17
|
+
end
|
18
|
+
|
19
|
+
status, headers, body = begin
|
20
|
+
@app.call(env)
|
21
|
+
ensure
|
22
|
+
ActiveSupport::Notifications.unsubscribe(subscriber)
|
23
|
+
end
|
24
|
+
|
25
|
+
header_info = events.group_by(&:name).map do |event_name, events_collection|
|
26
|
+
"#{event_name};dur=#{events_collection.sum(&:duration)}"
|
27
|
+
end
|
28
|
+
headers[SERVER_TIMING_HEADER] = header_info.join(", ")
|
29
|
+
|
30
|
+
[ status, headers, body ]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -8,7 +8,7 @@ require "action_dispatch/request/session"
|
|
8
8
|
|
9
9
|
module ActionDispatch
|
10
10
|
module Session
|
11
|
-
class SessionRestoreError < StandardError
|
11
|
+
class SessionRestoreError < StandardError # :nodoc:
|
12
12
|
def initialize
|
13
13
|
super("Session contains objects whose class definition isn't available.\n" \
|
14
14
|
"Remember to require the classes for all objects kept in the session.\n" \
|
@@ -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,8 +82,22 @@ module ActionDispatch
|
|
83
82
|
include SessionObject
|
84
83
|
|
85
84
|
private
|
85
|
+
def set_cookie(request, response, cookie)
|
86
|
+
request.cookie_jar[key] = cookie
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
class AbstractSecureStore < Rack::Session::Abstract::PersistedSecure
|
91
|
+
include Compatibility
|
92
|
+
include StaleSessionCheck
|
93
|
+
include SessionObject
|
94
|
+
|
95
|
+
def generate_sid
|
96
|
+
Rack::Session::SessionId.new(super)
|
97
|
+
end
|
86
98
|
|
87
|
-
|
99
|
+
private
|
100
|
+
def set_cookie(request, response, cookie)
|
88
101
|
request.cookie_jar[key] = cookie
|
89
102
|
end
|
90
103
|
end
|
@@ -12,7 +12,7 @@ module ActionDispatch
|
|
12
12
|
# * <tt>cache</tt> - The cache to use. If it is not specified, <tt>Rails.cache</tt> will be used.
|
13
13
|
# * <tt>expire_after</tt> - The length of time a session will be stored before automatically expiring.
|
14
14
|
# By default, the <tt>:expires_in</tt> option of the cache is used.
|
15
|
-
class CacheStore <
|
15
|
+
class CacheStore < AbstractSecureStore
|
16
16
|
def initialize(app, options = {})
|
17
17
|
@cache = options[:cache] || Rails.cache
|
18
18
|
options[:expire_after] ||= @cache.options[:expires_in]
|
@@ -21,7 +21,7 @@ module ActionDispatch
|
|
21
21
|
|
22
22
|
# Get a session from the cache.
|
23
23
|
def find_session(env, sid)
|
24
|
-
unless sid && (session =
|
24
|
+
unless sid && (session = get_session_with_fallback(sid))
|
25
25
|
sid, session = generate_sid, {}
|
26
26
|
end
|
27
27
|
[sid, session]
|
@@ -29,7 +29,7 @@ module ActionDispatch
|
|
29
29
|
|
30
30
|
# Set a session in the cache.
|
31
31
|
def write_session(env, sid, session, options)
|
32
|
-
key = cache_key(sid)
|
32
|
+
key = cache_key(sid.private_id)
|
33
33
|
if session
|
34
34
|
@cache.write(key, session, expires_in: options[:expire_after])
|
35
35
|
else
|
@@ -40,14 +40,19 @@ module ActionDispatch
|
|
40
40
|
|
41
41
|
# Remove a session from the cache.
|
42
42
|
def delete_session(env, sid, options)
|
43
|
-
@cache.delete(cache_key(sid))
|
43
|
+
@cache.delete(cache_key(sid.private_id))
|
44
|
+
@cache.delete(cache_key(sid.public_id))
|
44
45
|
generate_sid
|
45
46
|
end
|
46
47
|
|
47
48
|
private
|
48
49
|
# Turn the session id into a cache key.
|
49
|
-
def cache_key(
|
50
|
-
"_session_id:#{
|
50
|
+
def cache_key(id)
|
51
|
+
"_session_id:#{id}"
|
52
|
+
end
|
53
|
+
|
54
|
+
def get_session_with_fallback(sid)
|
55
|
+
@cache.read(cache_key(sid.private_id)) || @cache.read(cache_key(sid.public_id))
|
51
56
|
end
|
52
57
|
end
|
53
58
|
end
|
@@ -10,28 +10,24 @@ 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
|
#
|
32
|
-
#
|
33
|
-
#
|
34
|
-
# encrypted in the
|
27
|
+
# In the development and test environments your application's secret key base is
|
28
|
+
# generated by Rails and stored in a temporary file in <tt>tmp/development_secret.txt</tt>.
|
29
|
+
# In all other environments, it is stored encrypted in the
|
30
|
+
# <tt>config/credentials.yml.enc</tt> file.
|
35
31
|
#
|
36
32
|
# If your application was not updated to Rails 5.2 defaults, the secret_key_base
|
37
33
|
# will be found in the old <tt>config/secrets.yml</tt> file.
|
@@ -50,7 +46,16 @@ module ActionDispatch
|
|
50
46
|
# would set the session cookie to expire automatically 14 days after creation.
|
51
47
|
# Other useful options include <tt>:key</tt>, <tt>:secure</tt> and
|
52
48
|
# <tt>:httponly</tt>.
|
53
|
-
class CookieStore <
|
49
|
+
class CookieStore < AbstractSecureStore
|
50
|
+
class SessionId < DelegateClass(Rack::Session::SessionId)
|
51
|
+
attr_reader :cookie_value
|
52
|
+
|
53
|
+
def initialize(session_id, cookie_value = {})
|
54
|
+
super(session_id)
|
55
|
+
@cookie_value = cookie_value
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
54
59
|
def initialize(app, options = {})
|
55
60
|
super(app, options.merge!(cookie_only: true))
|
56
61
|
end
|
@@ -58,7 +63,7 @@ module ActionDispatch
|
|
58
63
|
def delete_session(req, session_id, options)
|
59
64
|
new_sid = generate_sid unless options[:drop]
|
60
65
|
# Reset hash and Assign the new session id
|
61
|
-
req.set_header("action_dispatch.request.unsigned_session_cookie", new_sid ? { "session_id" => new_sid } : {})
|
66
|
+
req.set_header("action_dispatch.request.unsigned_session_cookie", new_sid ? { "session_id" => new_sid.public_id } : {})
|
62
67
|
new_sid
|
63
68
|
end
|
64
69
|
|
@@ -66,15 +71,15 @@ module ActionDispatch
|
|
66
71
|
stale_session_check! do
|
67
72
|
data = unpacked_cookie_data(req)
|
68
73
|
data = persistent_session_id!(data)
|
69
|
-
[data["session_id"], data]
|
74
|
+
[Rack::Session::SessionId.new(data["session_id"]), data]
|
70
75
|
end
|
71
76
|
end
|
72
77
|
|
73
78
|
private
|
74
|
-
|
75
79
|
def extract_session_id(req)
|
76
80
|
stale_session_check! do
|
77
|
-
unpacked_cookie_data(req)["session_id"]
|
81
|
+
sid = unpacked_cookie_data(req)["session_id"]
|
82
|
+
sid && Rack::Session::SessionId.new(sid)
|
78
83
|
end
|
79
84
|
end
|
80
85
|
|
@@ -92,13 +97,13 @@ module ActionDispatch
|
|
92
97
|
|
93
98
|
def persistent_session_id!(data, sid = nil)
|
94
99
|
data ||= {}
|
95
|
-
data["session_id"] ||= sid || generate_sid
|
100
|
+
data["session_id"] ||= sid || generate_sid.public_id
|
96
101
|
data
|
97
102
|
end
|
98
103
|
|
99
104
|
def write_session(req, sid, session_data, options)
|
100
|
-
session_data["session_id"] = sid
|
101
|
-
session_data
|
105
|
+
session_data["session_id"] = sid.public_id
|
106
|
+
SessionId.new(sid, session_data)
|
102
107
|
end
|
103
108
|
|
104
109
|
def set_cookie(request, session_id, cookie)
|
@@ -1,6 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "action_dispatch/http/request"
|
4
3
|
require "action_dispatch/middleware/exception_wrapper"
|
5
4
|
|
6
5
|
module ActionDispatch
|
@@ -15,14 +14,8 @@ module ActionDispatch
|
|
15
14
|
# If the application returns a "X-Cascade" pass response, this middleware
|
16
15
|
# will send an empty response as result with the correct status code.
|
17
16
|
# If any exception happens inside the exceptions app, this middleware
|
18
|
-
# catches the exceptions and returns a
|
17
|
+
# catches the exceptions and returns a failsafe response.
|
19
18
|
class ShowExceptions
|
20
|
-
FAILSAFE_RESPONSE = [500, { "Content-Type" => "text/plain" },
|
21
|
-
["500 Internal Server Error\n" \
|
22
|
-
"If you are the administrator of this website, then please read this web " \
|
23
|
-
"application's log file and/or the web server's log file to find out what " \
|
24
|
-
"went wrong."]]
|
25
|
-
|
26
19
|
def initialize(app, exceptions_app)
|
27
20
|
@app = app
|
28
21
|
@exceptions_app = exceptions_app
|
@@ -40,19 +33,35 @@ module ActionDispatch
|
|
40
33
|
end
|
41
34
|
|
42
35
|
private
|
43
|
-
|
44
36
|
def render_exception(request, exception)
|
45
37
|
backtrace_cleaner = request.get_header "action_dispatch.backtrace_cleaner"
|
46
38
|
wrapper = ExceptionWrapper.new(backtrace_cleaner, exception)
|
47
39
|
status = wrapper.status_code
|
48
|
-
request.set_header "action_dispatch.exception", wrapper.
|
40
|
+
request.set_header "action_dispatch.exception", wrapper.unwrapped_exception
|
49
41
|
request.set_header "action_dispatch.original_path", request.path_info
|
42
|
+
request.set_header "action_dispatch.original_request_method", request.raw_request_method
|
43
|
+
fallback_to_html_format_if_invalid_mime_type(request)
|
50
44
|
request.path_info = "/#{status}"
|
45
|
+
request.request_method = "GET"
|
51
46
|
response = @exceptions_app.call(request.env)
|
52
47
|
response[1]["X-Cascade"] == "pass" ? pass_response(status) : response
|
53
48
|
rescue Exception => failsafe_error
|
54
49
|
$stderr.puts "Error during failsafe response: #{failsafe_error}\n #{failsafe_error.backtrace * "\n "}"
|
55
|
-
|
50
|
+
|
51
|
+
[500, { "Content-Type" => "text/plain" },
|
52
|
+
["500 Internal Server Error\n" \
|
53
|
+
"If you are the administrator of this website, then please read this web " \
|
54
|
+
"application's log file and/or the web server's log file to find out what " \
|
55
|
+
"went wrong."]]
|
56
|
+
end
|
57
|
+
|
58
|
+
def fallback_to_html_format_if_invalid_mime_type(request)
|
59
|
+
# If the MIME type for the request is invalid then the
|
60
|
+
# @exceptions_app may not be able to handle it. To make it
|
61
|
+
# easier to handle, we switch to HTML.
|
62
|
+
request.formats
|
63
|
+
rescue ActionDispatch::Http::MimeNegotiation::InvalidType
|
64
|
+
request.set_header "HTTP_ACCEPT", "text/html"
|
56
65
|
end
|
57
66
|
|
58
67
|
def pass_response(status)
|