actionpack 7.1.5.1 → 8.1.2

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.
Files changed (177) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +308 -523
  3. data/README.rdoc +1 -1
  4. data/lib/abstract_controller/asset_paths.rb +6 -2
  5. data/lib/abstract_controller/base.rb +104 -105
  6. data/lib/abstract_controller/caching/fragments.rb +50 -53
  7. data/lib/abstract_controller/caching.rb +8 -3
  8. data/lib/abstract_controller/callbacks.rb +70 -62
  9. data/lib/abstract_controller/collector.rb +7 -7
  10. data/lib/abstract_controller/deprecator.rb +2 -0
  11. data/lib/abstract_controller/error.rb +2 -0
  12. data/lib/abstract_controller/helpers.rb +71 -84
  13. data/lib/abstract_controller/logger.rb +4 -1
  14. data/lib/abstract_controller/railties/routes_helpers.rb +2 -0
  15. data/lib/abstract_controller/rendering.rb +13 -13
  16. data/lib/abstract_controller/translation.rb +12 -13
  17. data/lib/abstract_controller/url_for.rb +8 -6
  18. data/lib/abstract_controller.rb +2 -0
  19. data/lib/action_controller/api/api_rendering.rb +2 -0
  20. data/lib/action_controller/api.rb +76 -72
  21. data/lib/action_controller/base.rb +199 -126
  22. data/lib/action_controller/caching.rb +16 -14
  23. data/lib/action_controller/deprecator.rb +2 -0
  24. data/lib/action_controller/form_builder.rb +21 -18
  25. data/lib/action_controller/log_subscriber.rb +23 -2
  26. data/lib/action_controller/metal/allow_browser.rb +133 -0
  27. data/lib/action_controller/metal/basic_implicit_render.rb +2 -0
  28. data/lib/action_controller/metal/conditional_get.rb +217 -175
  29. data/lib/action_controller/metal/content_security_policy.rb +25 -24
  30. data/lib/action_controller/metal/cookies.rb +4 -2
  31. data/lib/action_controller/metal/data_streaming.rb +72 -63
  32. data/lib/action_controller/metal/default_headers.rb +5 -3
  33. data/lib/action_controller/metal/etag_with_flash.rb +3 -1
  34. data/lib/action_controller/metal/etag_with_template_digest.rb +17 -15
  35. data/lib/action_controller/metal/exceptions.rb +16 -9
  36. data/lib/action_controller/metal/flash.rb +13 -14
  37. data/lib/action_controller/metal/head.rb +15 -11
  38. data/lib/action_controller/metal/helpers.rb +63 -55
  39. data/lib/action_controller/metal/http_authentication.rb +209 -201
  40. data/lib/action_controller/metal/implicit_render.rb +17 -15
  41. data/lib/action_controller/metal/instrumentation.rb +16 -14
  42. data/lib/action_controller/metal/live.rb +177 -128
  43. data/lib/action_controller/metal/logging.rb +6 -4
  44. data/lib/action_controller/metal/mime_responds.rb +151 -142
  45. data/lib/action_controller/metal/parameter_encoding.rb +34 -32
  46. data/lib/action_controller/metal/params_wrapper.rb +57 -59
  47. data/lib/action_controller/metal/permissions_policy.rb +22 -12
  48. data/lib/action_controller/metal/rate_limiting.rb +92 -0
  49. data/lib/action_controller/metal/redirecting.rb +213 -94
  50. data/lib/action_controller/metal/renderers.rb +78 -57
  51. data/lib/action_controller/metal/rendering.rb +111 -77
  52. data/lib/action_controller/metal/request_forgery_protection.rb +182 -143
  53. data/lib/action_controller/metal/rescue.rb +20 -9
  54. data/lib/action_controller/metal/streaming.rb +118 -195
  55. data/lib/action_controller/metal/strong_parameters.rb +720 -530
  56. data/lib/action_controller/metal/testing.rb +2 -0
  57. data/lib/action_controller/metal/url_for.rb +17 -15
  58. data/lib/action_controller/metal.rb +86 -60
  59. data/lib/action_controller/railtie.rb +36 -15
  60. data/lib/action_controller/railties/helpers.rb +2 -0
  61. data/lib/action_controller/renderer.rb +41 -36
  62. data/lib/action_controller/structured_event_subscriber.rb +116 -0
  63. data/lib/action_controller/template_assertions.rb +4 -2
  64. data/lib/action_controller/test_case.rb +160 -131
  65. data/lib/action_controller.rb +5 -1
  66. data/lib/action_dispatch/constants.rb +8 -0
  67. data/lib/action_dispatch/deprecator.rb +2 -0
  68. data/lib/action_dispatch/http/cache.rb +163 -35
  69. data/lib/action_dispatch/http/content_disposition.rb +2 -0
  70. data/lib/action_dispatch/http/content_security_policy.rb +54 -39
  71. data/lib/action_dispatch/http/filter_parameters.rb +14 -8
  72. data/lib/action_dispatch/http/filter_redirect.rb +22 -1
  73. data/lib/action_dispatch/http/headers.rb +22 -22
  74. data/lib/action_dispatch/http/mime_negotiation.rb +89 -41
  75. data/lib/action_dispatch/http/mime_type.rb +25 -21
  76. data/lib/action_dispatch/http/mime_types.rb +3 -0
  77. data/lib/action_dispatch/http/param_builder.rb +187 -0
  78. data/lib/action_dispatch/http/param_error.rb +26 -0
  79. data/lib/action_dispatch/http/parameters.rb +14 -12
  80. data/lib/action_dispatch/http/permissions_policy.rb +25 -36
  81. data/lib/action_dispatch/http/query_parser.rb +55 -0
  82. data/lib/action_dispatch/http/rack_cache.rb +2 -0
  83. data/lib/action_dispatch/http/request.rb +141 -92
  84. data/lib/action_dispatch/http/response.rb +137 -77
  85. data/lib/action_dispatch/http/upload.rb +18 -16
  86. data/lib/action_dispatch/http/url.rb +187 -89
  87. data/lib/action_dispatch/journey/formatter.rb +21 -9
  88. data/lib/action_dispatch/journey/gtg/builder.rb +4 -3
  89. data/lib/action_dispatch/journey/gtg/simulator.rb +34 -11
  90. data/lib/action_dispatch/journey/gtg/transition_table.rb +47 -53
  91. data/lib/action_dispatch/journey/nfa/dot.rb +2 -0
  92. data/lib/action_dispatch/journey/nodes/node.rb +8 -6
  93. data/lib/action_dispatch/journey/parser.rb +99 -195
  94. data/lib/action_dispatch/journey/path/pattern.rb +4 -1
  95. data/lib/action_dispatch/journey/route.rb +54 -38
  96. data/lib/action_dispatch/journey/router/utils.rb +22 -27
  97. data/lib/action_dispatch/journey/router.rb +63 -83
  98. data/lib/action_dispatch/journey/routes.rb +11 -2
  99. data/lib/action_dispatch/journey/scanner.rb +46 -42
  100. data/lib/action_dispatch/journey/visitors.rb +57 -23
  101. data/lib/action_dispatch/journey/visualizer/fsm.js +4 -6
  102. data/lib/action_dispatch/journey.rb +2 -0
  103. data/lib/action_dispatch/log_subscriber.rb +7 -1
  104. data/lib/action_dispatch/middleware/actionable_exceptions.rb +2 -0
  105. data/lib/action_dispatch/middleware/assume_ssl.rb +8 -5
  106. data/lib/action_dispatch/middleware/callbacks.rb +3 -1
  107. data/lib/action_dispatch/middleware/cookies.rb +125 -106
  108. data/lib/action_dispatch/middleware/debug_exceptions.rb +37 -8
  109. data/lib/action_dispatch/middleware/debug_locks.rb +15 -13
  110. data/lib/action_dispatch/middleware/debug_view.rb +13 -5
  111. data/lib/action_dispatch/middleware/exception_wrapper.rb +18 -23
  112. data/lib/action_dispatch/middleware/executor.rb +19 -4
  113. data/lib/action_dispatch/middleware/flash.rb +63 -51
  114. data/lib/action_dispatch/middleware/host_authorization.rb +17 -15
  115. data/lib/action_dispatch/middleware/public_exceptions.rb +14 -12
  116. data/lib/action_dispatch/middleware/reloader.rb +5 -3
  117. data/lib/action_dispatch/middleware/remote_ip.rb +87 -77
  118. data/lib/action_dispatch/middleware/request_id.rb +16 -10
  119. data/lib/action_dispatch/middleware/server_timing.rb +4 -2
  120. data/lib/action_dispatch/middleware/session/abstract_store.rb +2 -0
  121. data/lib/action_dispatch/middleware/session/cache_store.rb +30 -8
  122. data/lib/action_dispatch/middleware/session/cookie_store.rb +27 -26
  123. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +7 -3
  124. data/lib/action_dispatch/middleware/show_exceptions.rb +16 -16
  125. data/lib/action_dispatch/middleware/ssl.rb +53 -40
  126. data/lib/action_dispatch/middleware/stack.rb +11 -10
  127. data/lib/action_dispatch/middleware/static.rb +33 -31
  128. data/lib/action_dispatch/middleware/templates/rescues/_copy_button.html.erb +1 -0
  129. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +3 -5
  130. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +9 -5
  131. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +1 -0
  132. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +1 -0
  133. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +4 -0
  134. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +3 -0
  135. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +50 -0
  136. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +1 -0
  137. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +1 -0
  138. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +1 -0
  139. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +1 -0
  140. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +1 -0
  141. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +1 -1
  142. data/lib/action_dispatch/railtie.rb +23 -3
  143. data/lib/action_dispatch/request/session.rb +24 -21
  144. data/lib/action_dispatch/request/utils.rb +11 -3
  145. data/lib/action_dispatch/routing/endpoint.rb +2 -0
  146. data/lib/action_dispatch/routing/inspector.rb +85 -60
  147. data/lib/action_dispatch/routing/mapper.rb +1031 -851
  148. data/lib/action_dispatch/routing/polymorphic_routes.rb +69 -62
  149. data/lib/action_dispatch/routing/redirection.rb +47 -39
  150. data/lib/action_dispatch/routing/route_set.rb +79 -56
  151. data/lib/action_dispatch/routing/routes_proxy.rb +7 -4
  152. data/lib/action_dispatch/routing/url_for.rb +130 -125
  153. data/lib/action_dispatch/routing.rb +150 -148
  154. data/lib/action_dispatch/structured_event_subscriber.rb +20 -0
  155. data/lib/action_dispatch/system_test_case.rb +91 -81
  156. data/lib/action_dispatch/system_testing/browser.rb +16 -23
  157. data/lib/action_dispatch/system_testing/driver.rb +2 -0
  158. data/lib/action_dispatch/system_testing/server.rb +2 -0
  159. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +34 -23
  160. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +2 -0
  161. data/lib/action_dispatch/testing/assertion_response.rb +9 -7
  162. data/lib/action_dispatch/testing/assertions/response.rb +52 -25
  163. data/lib/action_dispatch/testing/assertions/routing.rb +168 -87
  164. data/lib/action_dispatch/testing/assertions.rb +2 -0
  165. data/lib/action_dispatch/testing/integration.rb +233 -223
  166. data/lib/action_dispatch/testing/request_encoder.rb +11 -9
  167. data/lib/action_dispatch/testing/test_helpers/page_dump_helper.rb +35 -0
  168. data/lib/action_dispatch/testing/test_process.rb +11 -8
  169. data/lib/action_dispatch/testing/test_request.rb +3 -1
  170. data/lib/action_dispatch/testing/test_response.rb +27 -26
  171. data/lib/action_dispatch.rb +36 -32
  172. data/lib/action_pack/gem_version.rb +6 -4
  173. data/lib/action_pack/version.rb +3 -1
  174. data/lib/action_pack.rb +17 -16
  175. metadata +36 -32
  176. data/lib/action_dispatch/journey/parser.y +0 -50
  177. data/lib/action_dispatch/journey/parser_extras.rb +0 -31
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # :markup: markdown
4
+
3
5
  module ActionDispatch
4
6
  module Http
5
7
  module Cache
@@ -7,6 +9,8 @@ module ActionDispatch
7
9
  HTTP_IF_MODIFIED_SINCE = "HTTP_IF_MODIFIED_SINCE"
8
10
  HTTP_IF_NONE_MATCH = "HTTP_IF_NONE_MATCH"
9
11
 
12
+ mattr_accessor :strict_freshness, default: false
13
+
10
14
  def if_modified_since
11
15
  if since = get_header(HTTP_IF_MODIFIED_SINCE)
12
16
  Time.rfc2822(since) rescue nil
@@ -32,19 +36,140 @@ module ActionDispatch
32
36
  end
33
37
  end
34
38
 
35
- # Check response freshness (+Last-Modified+ and ETag) against request
36
- # +If-Modified-Since+ and +If-None-Match+ conditions. If both headers are
37
- # supplied, both must match, or the request is not considered fresh.
39
+ # Check response freshness (`Last-Modified` and `ETag`) against request
40
+ # `If-Modified-Since` and `If-None-Match` conditions.
41
+ # If both headers are supplied, based on configuration, either `ETag` is preferred over `Last-Modified`
42
+ # or both are considered equally. You can adjust the preference with
43
+ # `config.action_dispatch.strict_freshness`.
44
+ # Reference: http://tools.ietf.org/html/rfc7232#section-6
38
45
  def fresh?(response)
39
- last_modified = if_modified_since
40
- etag = if_none_match
46
+ if Request.strict_freshness
47
+ if if_none_match
48
+ etag_matches?(response.etag)
49
+ elsif if_modified_since
50
+ not_modified?(response.last_modified)
51
+ else
52
+ false
53
+ end
54
+ else
55
+ last_modified = if_modified_since
56
+ etag = if_none_match
57
+
58
+ return false unless last_modified || etag
59
+
60
+ success = true
61
+ success &&= not_modified?(response.last_modified) if last_modified
62
+ success &&= etag_matches?(response.etag) if etag
63
+ success
64
+ end
65
+ end
66
+
67
+ def cache_control_directives
68
+ @cache_control_directives ||= CacheControlDirectives.new(get_header("HTTP_CACHE_CONTROL"))
69
+ end
41
70
 
42
- return false unless last_modified || etag
71
+ # Represents the HTTP Cache-Control header for requests,
72
+ # providing methods to access various cache control directives
73
+ # Reference: https://www.rfc-editor.org/rfc/rfc9111.html#name-request-directives
74
+ class CacheControlDirectives
75
+ def initialize(cache_control_header)
76
+ @only_if_cached = false
77
+ @no_cache = false
78
+ @no_store = false
79
+ @no_transform = false
80
+ @max_age = nil
81
+ @max_stale = nil
82
+ @min_fresh = nil
83
+ @stale_if_error = false
84
+ parse_directives(cache_control_header)
85
+ end
86
+
87
+ # Returns true if the only-if-cached directive is present.
88
+ # This directive indicates that the client only wishes to obtain a
89
+ # stored response. If a valid stored response is not available,
90
+ # the server should respond with a 504 (Gateway Timeout) status.
91
+ def only_if_cached?
92
+ @only_if_cached
93
+ end
43
94
 
44
- success = true
45
- success &&= not_modified?(response.last_modified) if last_modified
46
- success &&= etag_matches?(response.etag) if etag
47
- success
95
+ # Returns true if the no-cache directive is present.
96
+ # This directive indicates that a cache must not use the response
97
+ # to satisfy subsequent requests without successful validation on the origin server.
98
+ def no_cache?
99
+ @no_cache
100
+ end
101
+
102
+ # Returns true if the no-store directive is present.
103
+ # This directive indicates that a cache must not store any part of the
104
+ # request or response.
105
+ def no_store?
106
+ @no_store
107
+ end
108
+
109
+ # Returns true if the no-transform directive is present.
110
+ # This directive indicates that a cache or proxy must not transform the payload.
111
+ def no_transform?
112
+ @no_transform
113
+ end
114
+
115
+ # Returns the value of the max-age directive.
116
+ # This directive indicates that the client is willing to accept a response
117
+ # whose age is no greater than the specified number of seconds.
118
+ attr_reader :max_age
119
+
120
+ # Returns the value of the max-stale directive.
121
+ # When max-stale is present with a value, returns that integer value.
122
+ # When max-stale is present without a value, returns true (unlimited staleness).
123
+ # When max-stale is not present, returns nil.
124
+ attr_reader :max_stale
125
+
126
+ # Returns true if max-stale directive is present (with or without a value)
127
+ def max_stale?
128
+ !@max_stale.nil?
129
+ end
130
+
131
+ # Returns true if max-stale directive is present without a value (unlimited staleness)
132
+ def max_stale_unlimited?
133
+ @max_stale == true
134
+ end
135
+
136
+ # Returns the value of the min-fresh directive.
137
+ # This directive indicates that the client is willing to accept a response
138
+ # whose freshness lifetime is no less than its current age plus the specified time in seconds.
139
+ attr_reader :min_fresh
140
+
141
+ # Returns the value of the stale-if-error directive.
142
+ # This directive indicates that the client is willing to accept a stale response
143
+ # if the check for a fresh one fails with an error for the specified number of seconds.
144
+ attr_reader :stale_if_error
145
+
146
+ private
147
+ def parse_directives(header_value)
148
+ return unless header_value
149
+
150
+ header_value.delete(" ").downcase.split(",").each do |directive|
151
+ name, value = directive.split("=", 2)
152
+
153
+ case name
154
+ when "max-age"
155
+ @max_age = value.to_i
156
+ when "min-fresh"
157
+ @min_fresh = value.to_i
158
+ when "stale-if-error"
159
+ @stale_if_error = value.to_i
160
+ when "no-cache"
161
+ @no_cache = true
162
+ when "no-store"
163
+ @no_store = true
164
+ when "no-transform"
165
+ @no_transform = true
166
+ when "only-if-cached"
167
+ @only_if_cached = true
168
+ when "max-stale"
169
+ @max_stale = value ? value.to_i : true
170
+ end
171
+ end
172
+ end
48
173
  end
49
174
  end
50
175
 
@@ -79,25 +204,24 @@ module ActionDispatch
79
204
  set_header DATE, utc_time.httpdate
80
205
  end
81
206
 
82
- # This method sets a weak ETag validator on the response so browsers
83
- # and proxies may cache the response, keyed on the ETag. On subsequent
84
- # requests, the +If-None-Match+ header is set to the cached ETag. If it
85
- # matches the current ETag, we can return a <tt>304 Not Modified</tt> response
86
- # with no body, letting the browser or proxy know that their cache is
87
- # current. Big savings in request time and network bandwidth.
207
+ # This method sets a weak ETag validator on the response so browsers and proxies
208
+ # may cache the response, keyed on the ETag. On subsequent requests, the
209
+ # `If-None-Match` header is set to the cached ETag. If it matches the current
210
+ # ETag, we can return a `304 Not Modified` response with no body, letting the
211
+ # browser or proxy know that their cache is current. Big savings in request time
212
+ # and network bandwidth.
88
213
  #
89
- # Weak ETags are considered to be semantically equivalent but not
90
- # byte-for-byte identical. This is perfect for browser caching of HTML
91
- # pages where we don't care about exact equality, just what the user
92
- # is viewing.
214
+ # Weak ETags are considered to be semantically equivalent but not byte-for-byte
215
+ # identical. This is perfect for browser caching of HTML pages where we don't
216
+ # care about exact equality, just what the user is viewing.
93
217
  #
94
- # Strong ETags are considered byte-for-byte identical. They allow a
95
- # browser or proxy cache to support +Range+ requests, useful for paging
96
- # through a PDF file or scrubbing through a video. Some CDNs only
97
- # support strong ETags and will ignore weak ETags entirely.
218
+ # Strong ETags are considered byte-for-byte identical. They allow a browser or
219
+ # proxy cache to support `Range` requests, useful for paging through a PDF file
220
+ # or scrubbing through a video. Some CDNs only support strong ETags and will
221
+ # ignore weak ETags entirely.
98
222
  #
99
- # Weak ETags are what we almost always need, so they're the default.
100
- # Check out #strong_etag= to provide a strong ETag validator.
223
+ # Weak ETags are what we almost always need, so they're the default. Check out
224
+ # #strong_etag= to provide a strong ETag validator.
101
225
  def etag=(weak_validators)
102
226
  self.weak_etag = weak_validators
103
227
  end
@@ -112,12 +236,13 @@ module ActionDispatch
112
236
 
113
237
  def etag?; etag; end
114
238
 
115
- # True if an ETag is set, and it's a weak validator (preceded with <tt>W/</tt>).
239
+ # True if an ETag is set, and it's a weak validator (preceded with `W/`).
116
240
  def weak_etag?
117
241
  etag? && etag.start_with?('W/"')
118
242
  end
119
243
 
120
- # True if an ETag is set, and it isn't a weak validator (not preceded with <tt>W/</tt>).
244
+ # True if an ETag is set, and it isn't a weak validator (not preceded with
245
+ # `W/`).
121
246
  def strong_etag?
122
247
  etag? && !weak_etag?
123
248
  end
@@ -125,7 +250,7 @@ module ActionDispatch
125
250
  private
126
251
  DATE = "Date"
127
252
  LAST_MODIFIED = "Last-Modified"
128
- SPECIAL_KEYS = Set.new(%w[extras no-store no-cache max-age public private must-revalidate])
253
+ SPECIAL_KEYS = Set.new(%w[extras no-store no-cache max-age public private must-revalidate must-understand])
129
254
 
130
255
  def generate_weak_etag(validators)
131
256
  "W/#{generate_strong_etag(validators)}"
@@ -169,12 +294,13 @@ module ActionDispatch
169
294
  PUBLIC = "public"
170
295
  PRIVATE = "private"
171
296
  MUST_REVALIDATE = "must-revalidate"
297
+ IMMUTABLE = "immutable"
298
+ MUST_UNDERSTAND = "must-understand"
172
299
 
173
300
  def handle_conditional_get!
174
- # Normally default cache control setting is handled by ETag
175
- # middleware. But, if an etag is already set, the middleware
176
- # defaults to `no-cache` unless a default `Cache-Control` value is
177
- # previously set. So, set a default one here.
301
+ # Normally default cache control setting is handled by ETag middleware. But, if
302
+ # an etag is already set, the middleware defaults to `no-cache` unless a default
303
+ # `Cache-Control` value is previously set. So, set a default one here.
178
304
  if (etag? || last_modified?) && !self._cache_control
179
305
  self._cache_control = DEFAULT_CACHE_CONTROL
180
306
  end
@@ -186,8 +312,8 @@ module ActionDispatch
186
312
  return if control.empty? && cache_control.empty? # Let middleware handle default behavior
187
313
 
188
314
  if cache_control.any?
189
- # Any caching directive coming from a controller overrides
190
- # no-cache/no-store in the default Cache-Control header.
315
+ # Any caching directive coming from a controller overrides no-cache/no-store in
316
+ # the default Cache-Control header.
191
317
  control.delete(:no_cache)
192
318
  control.delete(:no_store)
193
319
 
@@ -204,6 +330,7 @@ module ActionDispatch
204
330
 
205
331
  if control[:no_store]
206
332
  options << PRIVATE if control[:private]
333
+ options << MUST_UNDERSTAND if control[:must_understand]
207
334
  options << NO_STORE
208
335
  elsif control[:no_cache]
209
336
  options << PUBLIC if control[:public]
@@ -220,6 +347,7 @@ module ActionDispatch
220
347
  options << MUST_REVALIDATE if control[:must_revalidate]
221
348
  options << "stale-while-revalidate=#{stale_while_revalidate.to_i}" if stale_while_revalidate
222
349
  options << "stale-if-error=#{stale_if_error.to_i}" if stale_if_error
350
+ options << IMMUTABLE if control[:immutable]
223
351
  options.concat(extras) if extras
224
352
  end
225
353
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # :markup: markdown
4
+
3
5
  module ActionDispatch
4
6
  module Http
5
7
  class ContentDisposition # :nodoc:
@@ -1,28 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # :markup: markdown
4
+
3
5
  require "active_support/core_ext/object/deep_dup"
4
6
  require "active_support/core_ext/array/wrap"
5
7
 
6
8
  module ActionDispatch # :nodoc:
7
- # = Action Dispatch Content Security Policy
9
+ # # Action Dispatch Content Security Policy
8
10
  #
9
- # Configures the HTTP
10
- # {Content-Security-Policy}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy]
11
- # response header to help protect against XSS and injection attacks.
11
+ # Configures the HTTP [Content-Security-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy)
12
+ # response header to help protect against XSS and
13
+ # injection attacks.
12
14
  #
13
15
  # Example global policy:
14
16
  #
15
- # Rails.application.config.content_security_policy do |policy|
16
- # policy.default_src :self, :https
17
- # policy.font_src :self, :https, :data
18
- # policy.img_src :self, :https, :data
19
- # policy.object_src :none
20
- # policy.script_src :self, :https
21
- # policy.style_src :self, :https
17
+ # Rails.application.config.content_security_policy do |policy|
18
+ # policy.default_src :self, :https
19
+ # policy.font_src :self, :https, :data
20
+ # policy.img_src :self, :https, :data
21
+ # policy.object_src :none
22
+ # policy.script_src :self, :https
23
+ # policy.style_src :self, :https
22
24
  #
23
- # # Specify URI for violation reports
24
- # policy.report_uri "/csp-violation-report-endpoint"
25
- # end
25
+ # # Specify URI for violation reports
26
+ # policy.report_uri "/csp-violation-report-endpoint"
27
+ # end
26
28
  class ContentSecurityPolicy
27
29
  class InvalidDirectiveError < StandardError
28
30
  end
@@ -35,8 +37,8 @@ module ActionDispatch # :nodoc:
35
37
  def call(env)
36
38
  status, headers, _ = response = @app.call(env)
37
39
 
38
- # Returning CSP headers with a 304 Not Modified is harmful, since nonces in the new
39
- # CSP headers might not match nonces in the cached HTML.
40
+ # Returning CSP headers with a 304 Not Modified is harmful, since nonces in the
41
+ # new CSP headers might not match nonces in the cached HTML.
40
42
  return response if status == 304
41
43
 
42
44
  return response if policy_present?(headers)
@@ -126,6 +128,7 @@ module ActionDispatch # :nodoc:
126
128
  MAPPINGS = {
127
129
  self: "'self'",
128
130
  unsafe_eval: "'unsafe-eval'",
131
+ wasm_unsafe_eval: "'wasm-unsafe-eval'",
129
132
  unsafe_hashes: "'unsafe-hashes'",
130
133
  unsafe_inline: "'unsafe-inline'",
131
134
  none: "'none'",
@@ -168,6 +171,8 @@ module ActionDispatch # :nodoc:
168
171
  worker_src: "worker-src"
169
172
  }.freeze
170
173
 
174
+ HASH_SOURCE_ALGORITHM_PREFIXES = ["sha256-", "sha384-", "sha512-"].freeze
175
+
171
176
  DEFAULT_NONCE_DIRECTIVES = %w[script-src style-src].freeze
172
177
 
173
178
  private_constant :MAPPINGS, :DIRECTIVES, :DEFAULT_NONCE_DIRECTIVES
@@ -193,14 +198,14 @@ module ActionDispatch # :nodoc:
193
198
  end
194
199
  end
195
200
 
196
- # Specify whether to prevent the user agent from loading any assets over
197
- # HTTP when the page uses HTTPS:
201
+ # Specify whether to prevent the user agent from loading any assets over HTTP
202
+ # when the page uses HTTPS:
198
203
  #
199
- # policy.block_all_mixed_content
204
+ # policy.block_all_mixed_content
200
205
  #
201
- # Pass +false+ to allow it again:
206
+ # Pass `false` to allow it again:
202
207
  #
203
- # policy.block_all_mixed_content false
208
+ # policy.block_all_mixed_content false
204
209
  #
205
210
  def block_all_mixed_content(enabled = true)
206
211
  if enabled
@@ -212,11 +217,11 @@ module ActionDispatch # :nodoc:
212
217
 
213
218
  # Restricts the set of plugins that can be embedded:
214
219
  #
215
- # policy.plugin_types "application/x-shockwave-flash"
220
+ # policy.plugin_types "application/x-shockwave-flash"
216
221
  #
217
222
  # Leave empty to allow all plugins:
218
223
  #
219
- # policy.plugin_types
224
+ # policy.plugin_types
220
225
  #
221
226
  def plugin_types(*types)
222
227
  if types.first
@@ -226,23 +231,23 @@ module ActionDispatch # :nodoc:
226
231
  end
227
232
  end
228
233
 
229
- # Enable the {report-uri}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/report-uri]
230
- # directive. Violation reports will be sent to the specified URI:
234
+ # Enable the [report-uri](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/report-uri)
235
+ # directive. Violation reports will be sent to the
236
+ # specified URI:
231
237
  #
232
- # policy.report_uri "/csp-violation-report-endpoint"
238
+ # policy.report_uri "/csp-violation-report-endpoint"
233
239
  #
234
240
  def report_uri(uri)
235
241
  @directives["report-uri"] = [uri]
236
242
  end
237
243
 
238
- # Specify asset types for which {Subresource Integrity}[https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity]
239
- # is required:
244
+ # Specify asset types for which [Subresource Integrity](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity) is required:
240
245
  #
241
- # policy.require_sri_for :script, :style
246
+ # policy.require_sri_for :script, :style
242
247
  #
243
248
  # Leave empty to not require Subresource Integrity:
244
249
  #
245
- # policy.require_sri_for
250
+ # policy.require_sri_for
246
251
  #
247
252
  def require_sri_for(*types)
248
253
  if types.first
@@ -252,18 +257,18 @@ module ActionDispatch # :nodoc:
252
257
  end
253
258
  end
254
259
 
255
- # Specify whether a {sandbox}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/sandbox]
260
+ # Specify whether a [sandbox](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/sandbox)
256
261
  # should be enabled for the requested resource:
257
262
  #
258
- # policy.sandbox
263
+ # policy.sandbox
259
264
  #
260
265
  # Values can be passed as arguments:
261
266
  #
262
- # policy.sandbox "allow-scripts", "allow-modals"
267
+ # policy.sandbox "allow-scripts", "allow-modals"
263
268
  #
264
- # Pass +false+ to disable the sandbox:
269
+ # Pass `false` to disable the sandbox:
265
270
  #
266
- # policy.sandbox false
271
+ # policy.sandbox false
267
272
  #
268
273
  def sandbox(*values)
269
274
  if values.empty?
@@ -277,11 +282,11 @@ module ActionDispatch # :nodoc:
277
282
 
278
283
  # Specify whether user agents should treat any assets over HTTP as HTTPS:
279
284
  #
280
- # policy.upgrade_insecure_requests
285
+ # policy.upgrade_insecure_requests
281
286
  #
282
- # Pass +false+ to disable it:
287
+ # Pass `false` to disable it:
283
288
  #
284
- # policy.upgrade_insecure_requests false
289
+ # policy.upgrade_insecure_requests false
285
290
  #
286
291
  def upgrade_insecure_requests(enabled = true)
287
292
  if enabled
@@ -302,7 +307,13 @@ module ActionDispatch # :nodoc:
302
307
  case source
303
308
  when Symbol
304
309
  apply_mapping(source)
305
- when String, Proc
310
+ when String
311
+ if hash_source?(source)
312
+ "'#{source}'"
313
+ else
314
+ source
315
+ end
316
+ when Proc
306
317
  source
307
318
  else
308
319
  raise ArgumentError, "Invalid content security policy source: #{source.inspect}"
@@ -371,5 +382,9 @@ module ActionDispatch # :nodoc:
371
382
  def nonce_directive?(directive, nonce_directives)
372
383
  nonce_directives.include?(directive)
373
384
  end
385
+
386
+ def hash_source?(source)
387
+ source.start_with?(*HASH_SOURCE_ALGORITHM_PREFIXES)
388
+ end
374
389
  end
375
390
  end
@@ -1,22 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # :markup: markdown
4
+
3
5
  require "active_support/parameter_filter"
4
6
 
5
7
  module ActionDispatch
6
8
  module Http
7
- # = Action Dispatch HTTP Filter Parameters
9
+ # # Action Dispatch HTTP Filter Parameters
8
10
  #
9
11
  # Allows you to specify sensitive query string and POST parameters to filter
10
12
  # from the request log.
11
13
  #
12
- # # Replaces values with "[FILTERED]" for keys that match /foo|bar/i.
13
- # env["action_dispatch.parameter_filter"] = [:foo, "bar"]
14
+ # # Replaces values with "[FILTERED]" for keys that match /foo|bar/i.
15
+ # env["action_dispatch.parameter_filter"] = [:foo, "bar"]
14
16
  #
15
- # For more information about filter behavior, see ActiveSupport::ParameterFilter.
17
+ # For more information about filter behavior, see
18
+ # ActiveSupport::ParameterFilter.
16
19
  module FilterParameters
17
- ENV_MATCH = [/RAW_POST_DATA/, "rack.request.form_vars"] # :nodoc:
18
- NULL_PARAM_FILTER = ActiveSupport::ParameterFilter.new # :nodoc:
19
- NULL_ENV_FILTER = ActiveSupport::ParameterFilter.new ENV_MATCH # :nodoc:
20
+ # :stopdoc:
21
+ ENV_MATCH = [/RAW_POST_DATA/, "rack.request.form_vars"]
22
+ NULL_PARAM_FILTER = ActiveSupport::ParameterFilter.new
23
+ NULL_ENV_FILTER = ActiveSupport::ParameterFilter.new ENV_MATCH
24
+ # :startdoc:
20
25
 
21
26
  def initialize
22
27
  super
@@ -43,7 +48,8 @@ module ActionDispatch
43
48
  @filtered_path ||= query_string.empty? ? path : "#{path}?#{filtered_query_string}"
44
49
  end
45
50
 
46
- # Returns the +ActiveSupport::ParameterFilter+ object used to filter in this request.
51
+ # Returns the `ActiveSupport::ParameterFilter` object used to filter in this
52
+ # request.
47
53
  def parameter_filter
48
54
  @parameter_filter ||= if has_header?("action_dispatch.parameter_filter")
49
55
  parameter_filter_for get_header("action_dispatch.parameter_filter")
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # :markup: markdown
4
+
3
5
  module ActionDispatch
4
6
  module Http
5
7
  module FilterRedirect
@@ -9,7 +11,7 @@ module ActionDispatch
9
11
  if location_filter_match?
10
12
  FILTERED
11
13
  else
12
- location
14
+ parameter_filtered_location
13
15
  end
14
16
  end
15
17
 
@@ -31,6 +33,25 @@ module ActionDispatch
31
33
  end
32
34
  end
33
35
  end
36
+
37
+ def parameter_filtered_location
38
+ uri = URI.parse(location)
39
+ unless uri.query.nil? || uri.query.empty?
40
+ parts = uri.query.split(/([&;])/)
41
+ filtered_parts = parts.map do |part|
42
+ if part.include?("=")
43
+ key, value = part.split("=", 2)
44
+ request.parameter_filter.filter(key => value).first.join("=")
45
+ else
46
+ part
47
+ end
48
+ end
49
+ uri.query = filtered_parts.join("")
50
+ end
51
+ uri.to_s
52
+ rescue URI::Error
53
+ FILTERED
54
+ end
34
55
  end
35
56
  end
36
57
  end
@@ -1,28 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # :markup: markdown
4
+
3
5
  module ActionDispatch
4
6
  module Http
5
- # = Action Dispatch HTTP \Headers
7
+ # # Action Dispatch HTTP Headers
6
8
  #
7
9
  # Provides access to the request's HTTP headers from the environment.
8
10
  #
9
- # env = { "CONTENT_TYPE" => "text/plain", "HTTP_USER_AGENT" => "curl/7.43.0" }
10
- # headers = ActionDispatch::Http::Headers.from_hash(env)
11
- # headers["Content-Type"] # => "text/plain"
12
- # headers["User-Agent"] # => "curl/7.43.0"
11
+ # env = { "CONTENT_TYPE" => "text/plain", "HTTP_USER_AGENT" => "curl/7.43.0" }
12
+ # headers = ActionDispatch::Http::Headers.from_hash(env)
13
+ # headers["Content-Type"] # => "text/plain"
14
+ # headers["User-Agent"] # => "curl/7.43.0"
13
15
  #
14
16
  # Also note that when headers are mapped to CGI-like variables by the Rack
15
17
  # server, both dashes and underscores are converted to underscores. This
16
18
  # ambiguity cannot be resolved at this stage anymore. Both underscores and
17
19
  # dashes have to be interpreted as if they were originally sent as dashes.
18
20
  #
19
- # # GET / HTTP/1.1
20
- # # ...
21
- # # User-Agent: curl/7.43.0
22
- # # X_Custom_Header: token
21
+ # # GET / HTTP/1.1
22
+ # # ...
23
+ # # User-Agent: curl/7.43.0
24
+ # # X_Custom_Header: token
23
25
  #
24
- # headers["X_Custom_Header"] # => nil
25
- # headers["X-Custom-Header"] # => "token"
26
+ # headers["X_Custom_Header"] # => nil
27
+ # headers["X-Custom-Header"] # => "token"
26
28
  class Headers
27
29
  CGI_VARIABLES = Set.new(%W[
28
30
  AUTH_TYPE
@@ -67,7 +69,7 @@ module ActionDispatch
67
69
  @req.set_header env_name(key), value
68
70
  end
69
71
 
70
- # Add a value to a multivalued header like +Vary+ or +Accept-Encoding+.
72
+ # Add a value to a multivalued header like `Vary` or `Accept-Encoding`.
71
73
  def add(key, value)
72
74
  @req.add_header env_name(key), value
73
75
  end
@@ -81,11 +83,10 @@ module ActionDispatch
81
83
 
82
84
  # Returns the value for the given key mapped to @env.
83
85
  #
84
- # If the key is not found and an optional code block is not provided,
85
- # raises a <tt>KeyError</tt> exception.
86
+ # If the key is not found and an optional code block is not provided, raises a
87
+ # `KeyError` exception.
86
88
  #
87
- # If the code block is provided, then it will be run and
88
- # its result returned.
89
+ # If the code block is provided, then it will be run and its result returned.
89
90
  def fetch(key, default = DEFAULT)
90
91
  @req.fetch_header(env_name(key)) do
91
92
  return default unless default == DEFAULT
@@ -99,16 +100,15 @@ module ActionDispatch
99
100
  end
100
101
 
101
102
  # Returns a new Http::Headers instance containing the contents of
102
- # <tt>headers_or_env</tt> and the original instance.
103
+ # `headers_or_env` and the original instance.
103
104
  def merge(headers_or_env)
104
105
  headers = @req.dup.headers
105
106
  headers.merge!(headers_or_env)
106
107
  headers
107
108
  end
108
109
 
109
- # Adds the contents of <tt>headers_or_env</tt> to original instance
110
- # entries; duplicate keys are overwritten with the values from
111
- # <tt>headers_or_env</tt>.
110
+ # Adds the contents of `headers_or_env` to original instance entries; duplicate
111
+ # keys are overwritten with the values from `headers_or_env`.
112
112
  def merge!(headers_or_env)
113
113
  headers_or_env.each do |key, value|
114
114
  @req.set_header env_name(key), value
@@ -118,8 +118,8 @@ module ActionDispatch
118
118
  def env; @req.env.dup; end
119
119
 
120
120
  private
121
- # Converts an HTTP header name to an environment variable name if it is
122
- # not contained within the headers hash.
121
+ # Converts an HTTP header name to an environment variable name if it is not
122
+ # contained within the headers hash.
123
123
  def env_name(key)
124
124
  key = key.to_s
125
125
  if HTTP_HEADER.match?(key)