actionpack 6.0.3.7 → 6.0.4.3

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of actionpack might be problematic. Click here for more details.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 689cadf2c5055a30bce24daa0dbd2c5f4e9b62b8e05f318527556df6a1bbc0bd
4
- data.tar.gz: db971eb4537c26d3c61aaeb5933a936ab80465ab7533c3a1fd96fed34bc98159
3
+ metadata.gz: 4204052bc97219a103dc4fb43f06e381046d4ea1baf271d5b1223117996b0618
4
+ data.tar.gz: 7e78d49b9b56c77363b612e99ccccd467b079b45db0f66139b7f6b7ace5ab516
5
5
  SHA512:
6
- metadata.gz: bb3325660659b91dd7917b48dfd211078d28812bfae7695e224599573835d7c42994ead64ebeb5c121d1dbc4ac30da9ae0f3a0be304892c914ce2309d8b3fc2b
7
- data.tar.gz: 826160c64038886b1bc08301f8d4782cd1564f5b167d452bd87b8870043581dc971bb9f5a54075001fe254148fc8845187f660f4cf4fcb84f5dfdf17bfb85dda
6
+ metadata.gz: aeaa2c82e04fefe226a7b9fe789e1e306621ea54a1a3b92fca5cff2eb9e5d1fe9d16f73d23bb56942359fe6c3a6a8e5f242e778f7e22e381a53c229148ea4ef2
7
+ data.tar.gz: 36c20a636261420bd253b3ec129360c5cac2ccadebaff547634d2ca319911f9a55addea03ea1b99d3dacd636cc205c52103b138ba635b470c6de0c04946ca063
data/CHANGELOG.md CHANGED
@@ -1,3 +1,48 @@
1
+ ## Rails 6.0.4.3 (December 14, 2021) ##
2
+
3
+ * No changes.
4
+
5
+
6
+ ## Rails 6.0.4.2 (December 14, 2021) ##
7
+
8
+ * Fix X_FORWARDED_HOST protection. [CVE-2021-44528]
9
+
10
+ ## Rails 6.1.4.1 (August 19, 2021) ##
11
+
12
+ * [CVE-2021-22942] Fix possible open redirect in Host Authorization middleware.
13
+
14
+ Specially crafted "X-Forwarded-Host" headers in combination with certain
15
+ "allowed host" formats can cause the Host Authorization middleware in Action
16
+ Pack to redirect users to a malicious website.
17
+
18
+ ## Rails 6.0.4 (June 15, 2021) ##
19
+
20
+ * Accept base64_urlsafe CSRF tokens to make forward compatible.
21
+
22
+ Base64 strict-encoded CSRF tokens are not inherently websafe, which makes
23
+ them difficult to deal with. For example, the common practice of sending
24
+ the CSRF token to a browser in a client-readable cookie does not work properly
25
+ out of the box: the value has to be url-encoded and decoded to survive transport.
26
+
27
+ In Rails 6.1, we generate Base64 urlsafe-encoded CSRF tokens, which are inherently
28
+ safe to transport. Validation accepts both urlsafe tokens, and strict-encoded
29
+ tokens for backwards compatibility.
30
+
31
+ In Rails 5.2.5, the CSRF token format is accidentally changed to urlsafe-encoded.
32
+ If you upgrade apps from 5.2.5, set the config `urlsafe_csrf_tokens = true`.
33
+
34
+ ```ruby
35
+ Rails.application.config.action_controller.urlsafe_csrf_tokens = true
36
+ ```
37
+
38
+ *Scott Blum*, *Étienne Barrié*
39
+
40
+ * Signed and encrypted cookies can now store `false` as their value when
41
+ `action_dispatch.use_cookies_with_metadata` is enabled.
42
+
43
+ *Rolandas Barysas*
44
+
45
+
1
46
  ## Rails 6.0.3.7 (May 05, 2021) ##
2
47
 
3
48
  * Prevent catastrophic backtracking during mime parsing
@@ -53,6 +98,7 @@
53
98
 
54
99
  * [CVE-2020-8164] Return self when calling #each, #each_pair, and #each_value instead of the raw @parameters hash
55
100
 
101
+
56
102
  ## Rails 6.0.3 (May 06, 2020) ##
57
103
 
58
104
  * Include child session assertion count in ActionDispatch::IntegrationTest
data/README.rdoc CHANGED
@@ -33,7 +33,7 @@ The latest version of Action Pack can be installed with RubyGems:
33
33
 
34
34
  Source code can be downloaded as part of the Rails project on GitHub:
35
35
 
36
- * https://github.com/rails/rails/tree/master/actionpack
36
+ * https://github.com/rails/rails/tree/main/actionpack
37
37
 
38
38
 
39
39
  == License
@@ -275,7 +275,10 @@ module ActionController
275
275
  return false unless request.has_content_type?
276
276
 
277
277
  ref = request.content_mime_type.ref
278
+
278
279
  _wrapper_formats.include?(ref) && _wrapper_key && !request.parameters.key?(_wrapper_key)
280
+ rescue ActionDispatch::Http::Parameters::ParseError
281
+ false
279
282
  end
280
283
 
281
284
  def _perform_parameter_wrapping
@@ -289,8 +292,6 @@ module ActionController
289
292
 
290
293
  # This will display the wrapped hash in the log file.
291
294
  request.filtered_parameters.merge! wrapped_filtered_hash
292
- rescue ActionDispatch::Http::Parameters::ParseError
293
- # swallow parse error exception
294
295
  end
295
296
  end
296
297
  end
@@ -32,29 +32,21 @@ module ActionController #:nodoc:
32
32
  # response may be extracted. To prevent this, only XmlHttpRequest (known as XHR or
33
33
  # Ajax) requests are allowed to make requests for JavaScript responses.
34
34
  #
35
- # It's important to remember that XML or JSON requests are also checked by default. If
36
- # you're building an API or an SPA you could change forgery protection method in
37
- # <tt>ApplicationController</tt> (by default: <tt>:exception</tt>):
35
+ # Subclasses of <tt>ActionController::Base</tt> are protected by default with the
36
+ # <tt>:exception</tt> strategy, which raises an
37
+ # <tt>ActionController::InvalidAuthenticityToken</tt> error on unverified requests.
38
+ #
39
+ # APIs may want to disable this behavior since they are typically designed to be
40
+ # state-less: that is, the request API client handles the session instead of Rails.
41
+ # One way to achieve this is to use the <tt>:null_session</tt> strategy instead,
42
+ # which allows unverified requests to be handled, but with an empty session:
38
43
  #
39
44
  # class ApplicationController < ActionController::Base
40
- # protect_from_forgery unless: -> { request.format.json? }
45
+ # protect_from_forgery with: :null_session
41
46
  # end
42
47
  #
43
- # It is generally safe to exclude XHR requests from CSRF protection
44
- # (like the code snippet above does), because XHR requests can only be made from
45
- # the same origin. Note however that any cross-origin third party domain
46
- # allowed via {CORS}[https://en.wikipedia.org/wiki/Cross-origin_resource_sharing]
47
- # will also be able to create XHR requests. Be sure to check your
48
- # CORS configuration before disabling forgery protection for XHR.
49
- #
50
- # CSRF protection is turned on with the <tt>protect_from_forgery</tt> method.
51
- # By default <tt>protect_from_forgery</tt> protects your session with
52
- # <tt>:null_session</tt> method, which provides an empty session
53
- # during request.
54
- #
55
- # We may want to disable CSRF protection for APIs since they are typically
56
- # designed to be state-less. That is, the request API client will handle
57
- # the session for you instead of Rails.
48
+ # Note that API only applications don't include this module or a session middleware
49
+ # by default, and so don't require CSRF protection to be configured.
58
50
  #
59
51
  # The token parameter is named <tt>authenticity_token</tt> by default. The name and
60
52
  # value of this token must be added to every layout that renders forms by including
@@ -98,6 +90,10 @@ module ActionController #:nodoc:
98
90
  config_accessor :default_protect_from_forgery
99
91
  self.default_protect_from_forgery = false
100
92
 
93
+ # Controls whether URL-safe CSRF tokens are generated.
94
+ config_accessor :urlsafe_csrf_tokens, instance_writer: false
95
+ self.urlsafe_csrf_tokens = false
96
+
101
97
  helper_method :form_authenticity_token
102
98
  helper_method :protect_against_forgery?
103
99
  end
@@ -337,7 +333,7 @@ module ActionController #:nodoc:
337
333
  end
338
334
 
339
335
  begin
340
- masked_token = Base64.strict_decode64(encoded_masked_token)
336
+ masked_token = decode_csrf_token(encoded_masked_token)
341
337
  rescue ArgumentError # encoded_masked_token is invalid Base64
342
338
  return false
343
339
  end
@@ -375,7 +371,7 @@ module ActionController #:nodoc:
375
371
  one_time_pad = SecureRandom.random_bytes(AUTHENTICITY_TOKEN_LENGTH)
376
372
  encrypted_csrf_token = xor_byte_strings(one_time_pad, raw_token)
377
373
  masked_token = one_time_pad + encrypted_csrf_token
378
- Base64.strict_encode64(masked_token)
374
+ encode_csrf_token(masked_token)
379
375
  end
380
376
 
381
377
  def compare_with_real_token(token, session) # :doc:
@@ -401,8 +397,8 @@ module ActionController #:nodoc:
401
397
  end
402
398
 
403
399
  def real_csrf_token(session) # :doc:
404
- session[:_csrf_token] ||= SecureRandom.base64(AUTHENTICITY_TOKEN_LENGTH)
405
- Base64.strict_decode64(session[:_csrf_token])
400
+ session[:_csrf_token] ||= generate_csrf_token
401
+ decode_csrf_token(session[:_csrf_token])
406
402
  end
407
403
 
408
404
  def per_form_csrf_token(session, action_path, method) # :doc:
@@ -470,5 +466,33 @@ module ActionController #:nodoc:
470
466
  uri = URI.parse(action_path)
471
467
  uri.path.chomp("/")
472
468
  end
469
+
470
+ def generate_csrf_token # :nodoc:
471
+ if urlsafe_csrf_tokens
472
+ SecureRandom.urlsafe_base64(AUTHENTICITY_TOKEN_LENGTH, padding: false)
473
+ else
474
+ SecureRandom.base64(AUTHENTICITY_TOKEN_LENGTH)
475
+ end
476
+ end
477
+
478
+ def encode_csrf_token(csrf_token) # :nodoc:
479
+ if urlsafe_csrf_tokens
480
+ Base64.urlsafe_encode64(csrf_token, padding: false)
481
+ else
482
+ Base64.strict_encode64(csrf_token)
483
+ end
484
+ end
485
+
486
+ def decode_csrf_token(encoded_csrf_token) # :nodoc:
487
+ if urlsafe_csrf_tokens
488
+ Base64.urlsafe_decode64(encoded_csrf_token)
489
+ else
490
+ begin
491
+ Base64.strict_decode64(encoded_csrf_token)
492
+ rescue ArgumentError
493
+ Base64.urlsafe_decode64(encoded_csrf_token)
494
+ end
495
+ end
496
+ end
473
497
  end
474
498
  end
@@ -14,13 +14,13 @@ module ActionDispatch
14
14
  @filename = filename
15
15
  end
16
16
 
17
- TRADITIONAL_ESCAPED_CHAR = /[^ A-Za-z0-9!#$+.^_`|~-]/
17
+ TRADITIONAL_ESCAPED_CHAR = /[^ A-Za-z0-9!\#$+.^_`|~-]/
18
18
 
19
19
  def ascii_filename
20
20
  'filename="' + percent_escape(I18n.transliterate(filename), TRADITIONAL_ESCAPED_CHAR) + '"'
21
21
  end
22
22
 
23
- RFC_5987_ESCAPED_CHAR = /[^A-Za-z0-9!#$&+.^_`|~-]/
23
+ RFC_5987_ESCAPED_CHAR = /[^A-Za-z0-9!\#$&+.^_`|~-]/
24
24
 
25
25
  def utf8_filename
26
26
  "filename*=UTF-8''" + percent_escape(filename, RFC_5987_ESCAPED_CHAR)
@@ -7,6 +7,8 @@ module ActionDispatch
7
7
  module MimeNegotiation
8
8
  extend ActiveSupport::Concern
9
9
 
10
+ class InvalidType < ::Mime::Type::InvalidMimeType; end
11
+
10
12
  RESCUABLE_MIME_FORMAT_ERRORS = [
11
13
  ActionController::BadRequest,
12
14
  ActionDispatch::Http::Parameters::ParseError,
@@ -25,6 +27,8 @@ module ActionDispatch
25
27
  nil
26
28
  end
27
29
  set_header k, v
30
+ rescue ::Mime::Type::InvalidMimeType => e
31
+ raise InvalidType, e.message
28
32
  end
29
33
  end
30
34
 
@@ -47,6 +51,8 @@ module ActionDispatch
47
51
  Mime::Type.parse(header)
48
52
  end
49
53
  set_header k, v
54
+ rescue ::Mime::Type::InvalidMimeType => e
55
+ raise InvalidType, e.message
50
56
  end
51
57
  end
52
58
 
@@ -89,7 +89,7 @@ module ActionDispatch
89
89
  return params unless controller && controller.valid_encoding?
90
90
 
91
91
  if binary_params_for?(controller, action)
92
- ActionDispatch::Request::Utils.each_param_value(params) do |param|
92
+ ActionDispatch::Request::Utils.each_param_value(params.except(:controller, :action)) do |param|
93
93
  param.force_encoding ::Encoding::ASCII_8BIT
94
94
  end
95
95
  end
@@ -133,6 +133,8 @@ module ActionDispatch
133
133
  HTTP_METHOD_LOOKUP[method] = method.underscore.to_sym
134
134
  }
135
135
 
136
+ alias raw_request_method request_method # :nodoc:
137
+
136
138
  # Returns the HTTP \method that the application should see.
137
139
  # In the case where the \method was overridden by a middleware
138
140
  # (for instance, if a HEAD request was converted to a GET,
@@ -33,7 +33,7 @@ module ActionDispatch
33
33
  if uri.relative? || uri.scheme == "http" || uri.scheme == "https"
34
34
  body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(location)}\">redirected</a>.</body></html>"
35
35
  else
36
- return [400, {"Content-Type" => "text/plain"}, ["Invalid redirection URI"]]
36
+ return [400, { "Content-Type" => "text/plain" }, ["Invalid redirection URI"]]
37
37
  end
38
38
 
39
39
  [302, {
@@ -458,7 +458,13 @@ module ActionDispatch
458
458
 
459
459
  def [](name)
460
460
  if data = @parent_jar[name.to_s]
461
- parse(name, data, purpose: "cookie.#{name}") || parse(name, data)
461
+ result = parse(name, data, purpose: "cookie.#{name}")
462
+
463
+ if result.nil?
464
+ parse(name, data)
465
+ else
466
+ result
467
+ end
462
468
  end
463
469
  end
464
470
 
@@ -63,8 +63,8 @@ module ActionDispatch
63
63
  if request.get_header("action_dispatch.show_detailed_exceptions")
64
64
  begin
65
65
  content_type = request.formats.first
66
- rescue Mime::Type::InvalidMimeType
67
- render_for_api_request(Mime[:text], wrapper)
66
+ rescue ActionDispatch::Http::MimeNegotiation::InvalidType
67
+ content_type = Mime[:text]
68
68
  end
69
69
 
70
70
  if api_request?(content_type)
@@ -12,7 +12,7 @@ module ActionDispatch
12
12
  "ActionController::UnknownHttpMethod" => :method_not_allowed,
13
13
  "ActionController::NotImplemented" => :not_implemented,
14
14
  "ActionController::UnknownFormat" => :not_acceptable,
15
- "Mime::Type::InvalidMimeType" => :not_acceptable,
15
+ "ActionDispatch::Http::MimeNegotiation::InvalidType" => :not_acceptable,
16
16
  "ActionController::MissingExactTemplate" => :not_acceptable,
17
17
  "ActionController::InvalidAuthenticityToken" => :unprocessable_entity,
18
18
  "ActionController::InvalidCrossOriginRequest" => :unprocessable_entity,
@@ -10,6 +10,8 @@ module ActionDispatch
10
10
  # application will be executed and rendered. If no +response_app+ is given, a
11
11
  # default one will run, which responds with +403 Forbidden+.
12
12
  class HostAuthorization
13
+ ALLOWED_HOSTS_IN_DEVELOPMENT = [".localhost", /\A([a-z0-9-]+\.)?localhost:\d+\z/, IPAddr.new("0.0.0.0/0"), IPAddr.new("::/0")]
14
+
13
15
  class Permissions # :nodoc:
14
16
  def initialize(hosts)
15
17
  @hosts = sanitize_hosts(hosts)
@@ -46,9 +48,9 @@ module ActionDispatch
46
48
 
47
49
  def sanitize_string(host)
48
50
  if host.start_with?(".")
49
- /\A(.+\.)?#{Regexp.escape(host[1..-1])}\z/
51
+ /\A([a-z0-9-]+\.)?#{Regexp.escape(host[1..-1])}\z/i
50
52
  else
51
- host
53
+ /\A#{Regexp.escape host}\z/i
52
54
  end
53
55
  end
54
56
  end
@@ -87,20 +89,10 @@ module ActionDispatch
87
89
 
88
90
  private
89
91
  def authorized?(request)
90
- valid_host = /
91
- \A
92
- (?<host>[a-z0-9.-]+|\[[a-f0-9]*:[a-f0-9\.:]+\])
93
- (:\d+)?
94
- \z
95
- /x
96
-
97
- origin_host = valid_host.match(
98
- request.get_header("HTTP_HOST").to_s.downcase)
99
- forwarded_host = valid_host.match(
100
- request.x_forwarded_host.to_s.split(/,\s?/).last)
101
-
102
- origin_host && @permissions.allows?(origin_host[:host]) && (
103
- forwarded_host.nil? || @permissions.allows?(forwarded_host[:host]))
92
+ origin_host = request.get_header("HTTP_HOST")
93
+ forwarded_host = request.x_forwarded_host&.split(/,\s?/)&.last
94
+
95
+ @permissions.allows?(origin_host) && (forwarded_host.blank? || @permissions.allows?(forwarded_host))
104
96
  end
105
97
 
106
98
  def mark_as_authorized(request)
@@ -23,7 +23,7 @@ module ActionDispatch
23
23
  status = request.path_info[1..-1].to_i
24
24
  begin
25
25
  content_type = request.formats.first
26
- rescue Mime::Type::InvalidMimeType
26
+ rescue ActionDispatch::Http::MimeNegotiation::InvalidType
27
27
  content_type = Mime[:text]
28
28
  end
29
29
  body = { status: status, error: Rack::Utils::HTTP_STATUS_CODES.fetch(status, Rack::Utils::HTTP_STATUS_CODES[500]) }
@@ -6,6 +6,7 @@
6
6
  <%= @exception.message %>
7
7
  <% if defined?(ActiveStorage) && @exception.message.match?(%r{#{ActiveStorage::Blob.table_name}|#{ActiveStorage::Attachment.table_name}}) %>
8
8
  To resolve this issue run: rails active_storage:install
9
+ <% end %>
9
10
  <% if defined?(ActionMailbox) && @exception.message.match?(%r{#{ActionMailbox::InboundEmail.table_name}}) %>
10
11
  To resolve this issue run: rails action_mailbox:install
11
12
  <% end %>
@@ -107,6 +107,7 @@ module ActionDispatch
107
107
  @_routes = nil
108
108
  super
109
109
  end
110
+ ruby2_keywords(:initialize) if respond_to?(:ruby2_keywords, true)
110
111
 
111
112
  # Hook overridden in controller to add request information
112
113
  # with +default_url_options+. Application logic should not
@@ -64,14 +64,14 @@ module ActionDispatch
64
64
 
65
65
  private
66
66
  def headless_chrome_browser_options
67
- capabilities.args << "--headless"
68
- capabilities.args << "--disable-gpu" if Gem.win_platform?
67
+ capabilities.add_argument("--headless")
68
+ capabilities.add_argument("--disable-gpu") if Gem.win_platform?
69
69
 
70
70
  capabilities
71
71
  end
72
72
 
73
73
  def headless_firefox_browser_options
74
- capabilities.args << "-headless"
74
+ capabilities.add_argument("-headless")
75
75
 
76
76
  capabilities
77
77
  end
@@ -9,8 +9,8 @@ module ActionPack
9
9
  module VERSION
10
10
  MAJOR = 6
11
11
  MINOR = 0
12
- TINY = 3
13
- PRE = "7"
12
+ TINY = 4
13
+ PRE = "3"
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: actionpack
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.0.3.7
4
+ version: 6.0.4.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Heinemeier Hansson
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-05-05 00:00:00.000000000 Z
11
+ date: 2021-12-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - '='
18
18
  - !ruby/object:Gem::Version
19
- version: 6.0.3.7
19
+ version: 6.0.4.3
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - '='
25
25
  - !ruby/object:Gem::Version
26
- version: 6.0.3.7
26
+ version: 6.0.4.3
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rack
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -98,28 +98,28 @@ dependencies:
98
98
  requirements:
99
99
  - - '='
100
100
  - !ruby/object:Gem::Version
101
- version: 6.0.3.7
101
+ version: 6.0.4.3
102
102
  type: :runtime
103
103
  prerelease: false
104
104
  version_requirements: !ruby/object:Gem::Requirement
105
105
  requirements:
106
106
  - - '='
107
107
  - !ruby/object:Gem::Version
108
- version: 6.0.3.7
108
+ version: 6.0.4.3
109
109
  - !ruby/object:Gem::Dependency
110
110
  name: activemodel
111
111
  requirement: !ruby/object:Gem::Requirement
112
112
  requirements:
113
113
  - - '='
114
114
  - !ruby/object:Gem::Version
115
- version: 6.0.3.7
115
+ version: 6.0.4.3
116
116
  type: :development
117
117
  prerelease: false
118
118
  version_requirements: !ruby/object:Gem::Requirement
119
119
  requirements:
120
120
  - - '='
121
121
  - !ruby/object:Gem::Version
122
- version: 6.0.3.7
122
+ version: 6.0.4.3
123
123
  description: Web apps on Rails. Simple, battle-tested conventions for building and
124
124
  testing MVC web applications. Works with any Rack-compatible server.
125
125
  email: david@loudthinking.com
@@ -310,10 +310,10 @@ licenses:
310
310
  - MIT
311
311
  metadata:
312
312
  bug_tracker_uri: https://github.com/rails/rails/issues
313
- changelog_uri: https://github.com/rails/rails/blob/v6.0.3.7/actionpack/CHANGELOG.md
314
- documentation_uri: https://api.rubyonrails.org/v6.0.3.7/
313
+ changelog_uri: https://github.com/rails/rails/blob/v6.0.4.3/actionpack/CHANGELOG.md
314
+ documentation_uri: https://api.rubyonrails.org/v6.0.4.3/
315
315
  mailing_list_uri: https://discuss.rubyonrails.org/c/rubyonrails-talk
316
- source_code_uri: https://github.com/rails/rails/tree/v6.0.3.7/actionpack
316
+ source_code_uri: https://github.com/rails/rails/tree/v6.0.4.3/actionpack
317
317
  post_install_message:
318
318
  rdoc_options: []
319
319
  require_paths:
@@ -330,7 +330,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
330
330
  version: '0'
331
331
  requirements:
332
332
  - none
333
- rubygems_version: 3.1.2
333
+ rubygems_version: 3.2.15
334
334
  signing_key:
335
335
  specification_version: 4
336
336
  summary: Web-flow and rendering framework putting the VC in MVC (part of Rails).