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.

Files changed (167) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +264 -220
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +6 -6
  5. data/lib/abstract_controller/asset_paths.rb +1 -1
  6. data/lib/abstract_controller/base.rb +24 -4
  7. data/lib/abstract_controller/caching/fragments.rb +8 -24
  8. data/lib/abstract_controller/caching.rb +2 -2
  9. data/lib/abstract_controller/callbacks.rb +34 -8
  10. data/lib/abstract_controller/collector.rb +5 -4
  11. data/lib/abstract_controller/error.rb +1 -1
  12. data/lib/abstract_controller/helpers.rb +107 -90
  13. data/lib/abstract_controller/logger.rb +1 -1
  14. data/lib/abstract_controller/railties/routes_helpers.rb +19 -1
  15. data/lib/abstract_controller/rendering.rb +9 -9
  16. data/lib/abstract_controller/translation.rb +12 -5
  17. data/lib/abstract_controller/url_for.rb +4 -6
  18. data/lib/abstract_controller.rb +2 -0
  19. data/lib/action_controller/api.rb +5 -4
  20. data/lib/action_controller/base.rb +6 -9
  21. data/lib/action_controller/caching.rb +1 -3
  22. data/lib/action_controller/log_subscriber.rb +13 -9
  23. data/lib/action_controller/metal/basic_implicit_render.rb +1 -1
  24. data/lib/action_controller/metal/conditional_get.rb +57 -6
  25. data/lib/action_controller/metal/content_security_policy.rb +2 -3
  26. data/lib/action_controller/metal/cookies.rb +4 -2
  27. data/lib/action_controller/metal/data_streaming.rb +9 -18
  28. data/lib/action_controller/metal/default_headers.rb +17 -0
  29. data/lib/action_controller/metal/etag_with_template_digest.rb +4 -6
  30. data/lib/action_controller/metal/exceptions.rb +55 -12
  31. data/lib/action_controller/metal/flash.rb +10 -6
  32. data/lib/action_controller/metal/head.rb +7 -4
  33. data/lib/action_controller/metal/helpers.rb +15 -6
  34. data/lib/action_controller/metal/http_authentication.rb +41 -39
  35. data/lib/action_controller/metal/implicit_render.rb +5 -15
  36. data/lib/action_controller/metal/instrumentation.rb +59 -55
  37. data/lib/action_controller/metal/live.rb +80 -33
  38. data/lib/action_controller/metal/logging.rb +20 -0
  39. data/lib/action_controller/metal/mime_responds.rb +22 -7
  40. data/lib/action_controller/metal/parameter_encoding.rb +35 -4
  41. data/lib/action_controller/metal/params_wrapper.rb +50 -31
  42. data/lib/action_controller/metal/permissions_policy.rb +46 -0
  43. data/lib/action_controller/metal/redirecting.rb +93 -23
  44. data/lib/action_controller/metal/renderers.rb +4 -4
  45. data/lib/action_controller/metal/rendering.rb +14 -9
  46. data/lib/action_controller/metal/request_forgery_protection.rb +160 -58
  47. data/lib/action_controller/metal/rescue.rb +2 -2
  48. data/lib/action_controller/metal/streaming.rb +1 -4
  49. data/lib/action_controller/metal/strong_parameters.rb +236 -88
  50. data/lib/action_controller/metal/testing.rb +9 -2
  51. data/lib/action_controller/metal/url_for.rb +1 -1
  52. data/lib/action_controller/metal.rb +16 -17
  53. data/lib/action_controller/railtie.rb +49 -6
  54. data/lib/action_controller/railties/helpers.rb +1 -1
  55. data/lib/action_controller/renderer.rb +37 -13
  56. data/lib/action_controller/template_assertions.rb +1 -1
  57. data/lib/action_controller/test_case.rb +98 -68
  58. data/lib/action_controller.rb +4 -5
  59. data/lib/action_dispatch/http/cache.rb +45 -32
  60. data/lib/action_dispatch/http/content_disposition.rb +45 -0
  61. data/lib/action_dispatch/http/content_security_policy.rb +69 -56
  62. data/lib/action_dispatch/http/filter_parameters.rb +14 -8
  63. data/lib/action_dispatch/http/filter_redirect.rb +2 -3
  64. data/lib/action_dispatch/http/headers.rb +4 -4
  65. data/lib/action_dispatch/http/mime_negotiation.rb +44 -16
  66. data/lib/action_dispatch/http/mime_type.rb +47 -30
  67. data/lib/action_dispatch/http/parameters.rb +18 -27
  68. data/lib/action_dispatch/http/permissions_policy.rb +173 -0
  69. data/lib/action_dispatch/http/request.rb +49 -35
  70. data/lib/action_dispatch/http/response.rb +34 -26
  71. data/lib/action_dispatch/http/upload.rb +9 -1
  72. data/lib/action_dispatch/http/url.rb +86 -94
  73. data/lib/action_dispatch/journey/formatter.rb +55 -31
  74. data/lib/action_dispatch/journey/gtg/builder.rb +30 -46
  75. data/lib/action_dispatch/journey/gtg/simulator.rb +15 -8
  76. data/lib/action_dispatch/journey/gtg/transition_table.rb +78 -21
  77. data/lib/action_dispatch/journey/nfa/dot.rb +0 -11
  78. data/lib/action_dispatch/journey/nodes/node.rb +83 -16
  79. data/lib/action_dispatch/journey/parser.rb +13 -13
  80. data/lib/action_dispatch/journey/parser.y +1 -1
  81. data/lib/action_dispatch/journey/path/pattern.rb +42 -34
  82. data/lib/action_dispatch/journey/route.rb +14 -31
  83. data/lib/action_dispatch/journey/router/utils.rb +16 -14
  84. data/lib/action_dispatch/journey/router.rb +27 -35
  85. data/lib/action_dispatch/journey/routes.rb +3 -5
  86. data/lib/action_dispatch/journey/scanner.rb +10 -4
  87. data/lib/action_dispatch/journey/visitors.rb +1 -4
  88. data/lib/action_dispatch/journey/visualizer/fsm.js +49 -24
  89. data/lib/action_dispatch/journey/visualizer/index.html.erb +1 -1
  90. data/lib/action_dispatch/journey.rb +0 -2
  91. data/lib/action_dispatch/middleware/actionable_exceptions.rb +45 -0
  92. data/lib/action_dispatch/middleware/callbacks.rb +2 -4
  93. data/lib/action_dispatch/middleware/cookies.rb +136 -113
  94. data/lib/action_dispatch/middleware/debug_exceptions.rb +47 -68
  95. data/lib/action_dispatch/middleware/debug_locks.rb +8 -8
  96. data/lib/action_dispatch/middleware/debug_view.rb +66 -0
  97. data/lib/action_dispatch/middleware/exception_wrapper.rb +79 -30
  98. data/lib/action_dispatch/middleware/executor.rb +4 -1
  99. data/lib/action_dispatch/middleware/flash.rb +10 -12
  100. data/lib/action_dispatch/middleware/host_authorization.rb +159 -0
  101. data/lib/action_dispatch/middleware/public_exceptions.rb +6 -3
  102. data/lib/action_dispatch/middleware/remote_ip.rb +30 -20
  103. data/lib/action_dispatch/middleware/request_id.rb +5 -6
  104. data/lib/action_dispatch/middleware/server_timing.rb +33 -0
  105. data/lib/action_dispatch/middleware/session/abstract_store.rb +16 -3
  106. data/lib/action_dispatch/middleware/session/cache_store.rb +11 -6
  107. data/lib/action_dispatch/middleware/session/cookie_store.rb +24 -19
  108. data/lib/action_dispatch/middleware/show_exceptions.rb +20 -11
  109. data/lib/action_dispatch/middleware/ssl.rb +20 -15
  110. data/lib/action_dispatch/middleware/stack.rb +79 -7
  111. data/lib/action_dispatch/middleware/static.rb +150 -94
  112. data/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb +13 -0
  113. data/lib/action_dispatch/middleware/templates/rescues/_actions.text.erb +0 -0
  114. data/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb +22 -0
  115. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +6 -11
  116. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb +1 -1
  117. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +4 -2
  118. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +46 -36
  119. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +8 -0
  120. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +7 -0
  121. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +25 -6
  122. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +1 -1
  123. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +9 -6
  124. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +4 -1
  125. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +121 -15
  126. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +19 -0
  127. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb +3 -0
  128. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +5 -5
  129. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +4 -4
  130. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +5 -5
  131. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +4 -4
  132. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +16 -2
  133. data/lib/action_dispatch/railtie.rb +16 -4
  134. data/lib/action_dispatch/request/session.rb +59 -22
  135. data/lib/action_dispatch/request/utils.rb +28 -2
  136. data/lib/action_dispatch/routing/inspector.rb +102 -54
  137. data/lib/action_dispatch/routing/mapper.rb +184 -156
  138. data/lib/action_dispatch/routing/polymorphic_routes.rb +21 -19
  139. data/lib/action_dispatch/routing/redirection.rb +4 -6
  140. data/lib/action_dispatch/routing/route_set.rb +83 -73
  141. data/lib/action_dispatch/routing/routes_proxy.rb +1 -1
  142. data/lib/action_dispatch/routing/url_for.rb +2 -3
  143. data/lib/action_dispatch/routing.rb +23 -22
  144. data/lib/action_dispatch/system_test_case.rb +65 -16
  145. data/lib/action_dispatch/system_testing/browser.rb +43 -16
  146. data/lib/action_dispatch/system_testing/driver.rb +42 -10
  147. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +58 -12
  148. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +3 -10
  149. data/lib/action_dispatch/testing/assertion_response.rb +0 -1
  150. data/lib/action_dispatch/testing/assertions/response.rb +4 -7
  151. data/lib/action_dispatch/testing/assertions/routing.rb +20 -8
  152. data/lib/action_dispatch/testing/assertions.rb +3 -6
  153. data/lib/action_dispatch/testing/integration.rb +61 -30
  154. data/lib/action_dispatch/testing/request_encoder.rb +2 -2
  155. data/lib/action_dispatch/testing/test_process.rb +8 -6
  156. data/lib/action_dispatch/testing/test_request.rb +3 -3
  157. data/lib/action_dispatch/testing/test_response.rb +4 -32
  158. data/lib/action_dispatch.rb +15 -7
  159. data/lib/action_pack/gem_version.rb +4 -4
  160. data/lib/action_pack.rb +1 -1
  161. metadata +44 -25
  162. data/lib/action_controller/metal/force_ssl.rb +0 -99
  163. data/lib/action_dispatch/http/parameter_filter.rb +0 -86
  164. data/lib/action_dispatch/journey/nfa/builder.rb +0 -78
  165. data/lib/action_dispatch/journey/nfa/simulator.rb +0 -49
  166. data/lib/action_dispatch/journey/nfa/transition_table.rb +0 -120
  167. 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
- content_type = request.formats.first
25
- body = { status: status, error: Rack::Utils::HTTP_STATUS_CODES.fetch(status, Rack::Utils::HTTP_STATUS_CODES[500]) }
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}[http://blog.gingerlime.com/2012/rails-ip-spoofing-vulnerabilities-and-protection]
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}[http://andre.arko.net/2011/12/26/repeated-headers-and-ruby-web-servers].
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.1", # localhost IPv4
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 Array of string, IPAddr, or
55
- # Regexp objects which will be used instead of +TRUSTED_PROXIES+. If a
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}[http://blog.gingerlime.com/2012/rails-ip-spoofing-vulnerabilities-and-protection/],
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, remote_addr].flatten.compact
158
+ ips = [forwarded_ips, client_ips].flatten.compact
147
159
 
148
- # If every single IP option is in the trusted list, just return REMOTE_ADDR
149
- filter_proxies(ips).first || remote_addr
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
- begin
166
- # Only return IPs that are valid according to the IPAddr#new method.
167
- range = IPAddr.new(ip).to_range
168
- # We want to make sure nobody is sneaking a netmask in.
169
- range.begin == range.end
170
- rescue ArgumentError
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
- X_REQUEST_ID = "X-Request-Id".freeze #:nodoc:
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.x_request_id)
27
- @app.call(env).tap { |_status, headers, _body| headers[X_REQUEST_ID] = req.request_id }
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\-@]/, "".freeze).first(255)
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 #:nodoc:
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
- def set_cookie(request, session_id, cookie)
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 < AbstractStore
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 = @cache.read(cache_key(sid)))
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(sid)
50
- "_session_id:#{sid}"
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 4K cookie size limit. A CookieOverflow exception is raised if
14
- # you attempt to store more than 4K of data.
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 <tt>config/initializers/session_store.rb</tt>:
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
- # By default, your secret key base is derived from your application name in
33
- # the test and development environments. In all other environments, it is stored
34
- # encrypted in the <tt>config/credentials.yml.enc</tt> file.
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 < AbstractStore
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 FAILSAFE_RESPONSE.
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.exception
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
- FAILSAFE_RESPONSE
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)