diff options
Diffstat (limited to 'actionpack')
25 files changed, 698 insertions, 138 deletions
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index dec8e0cadf..b93ae8f8ff 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,3 +1,39 @@ +* More explicit error message when running `rake routes`. `CONTROLLER` argument + can now be supplied in different ways: + `Rails::WelcomeController`, `Rails::Welcome`, `rails/welcome` + + Fixes #22918 + + *Edouard Chin* + +* Allow `ActionController::Parameters` instances as an argument to URL + helper methods. An `ArgumentError` will be raised if the passed parameters + are not secure. + + Fixes #22832 + + *Prathamesh Sonpatki* + +* Add option for per-form CSRF tokens. + + *Greg Ose & Ben Toews* + +* Add tests and documentation for `ActionController::Renderers::use_renderers`. + + *Benjamin Fleischer* + +* Fix `ActionController::Parameters#convert_parameters_to_hashes` to return filtered + or unfiltered values based on from where it is called, `to_h` or `to_unsafe_h` + respectively. + + Fixes #22841 + + *Prathamesh Sonpatki* + +* Add `ActionController::Parameters#include?` + + *Justin Coyne* + ## Rails 5.0.0.beta1 (December 18, 2015) ## * Deprecate `redirect_to :back` in favor of `redirect_back`, which accepts a diff --git a/actionpack/MIT-LICENSE b/actionpack/MIT-LICENSE index 3ec7a617cf..8573eb1225 100644 --- a/actionpack/MIT-LICENSE +++ b/actionpack/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2015 David Heinemeier Hansson +Copyright (c) 2004-2016 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/actionpack/lib/action_controller/metal/conditional_get.rb b/actionpack/lib/action_controller/metal/conditional_get.rb index d86a793e4c..f8e0d9cf6c 100644 --- a/actionpack/lib/action_controller/metal/conditional_get.rb +++ b/actionpack/lib/action_controller/metal/conditional_get.rb @@ -228,7 +228,7 @@ module ActionController expires_in 100.years, public: public yield if stale?(etag: "#{version}-#{request.fullpath}", - last_modified: Time.parse('2011-01-01').utc, + last_modified: Time.new(2011, 1, 1).utc, public: public) end diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb index 22e0bb5955..90fb34e386 100644 --- a/actionpack/lib/action_controller/metal/renderers.rb +++ b/actionpack/lib/action_controller/metal/renderers.rb @@ -11,6 +11,7 @@ module ActionController Renderers.remove(key) end + # See <tt>Responder#api_behavior</tt> class MissingRenderer < LoadError def initialize(format) super "No renderer defined for format: #{format}" @@ -20,40 +21,25 @@ module ActionController module Renderers extend ActiveSupport::Concern + # A Set containing renderer names that correspond to available renderer procs. + # Default values are <tt>:json</tt>, <tt>:js</tt>, <tt>:xml</tt>. + RENDERERS = Set.new + included do class_attribute :_renderers self._renderers = Set.new.freeze end - module ClassMethods - def use_renderers(*args) - renderers = _renderers + args - self._renderers = renderers.freeze - end - alias use_renderer use_renderers - end - - def render_to_body(options) - _render_to_body_with_renderer(options) || super - end + # Used in <tt>ActionController::Base</tt> + # and <tt>ActionController::API</tt> to include all + # renderers by default. + module All + extend ActiveSupport::Concern + include Renderers - def _render_to_body_with_renderer(options) - _renderers.each do |name| - if options.key?(name) - _process_options(options) - method_name = Renderers._render_with_renderer_method_name(name) - return send(method_name, options.delete(name), options) - end + included do + self._renderers = RENDERERS end - nil - end - - # A Set containing renderer names that correspond to available renderer procs. - # Default values are <tt>:json</tt>, <tt>:js</tt>, <tt>:xml</tt>. - RENDERERS = Set.new - - def self._render_with_renderer_method_name(key) - "_render_with_renderer_#{key}" end # Adds a new renderer to call within controller actions. @@ -103,13 +89,70 @@ module ActionController remove_method(method_name) if method_defined?(method_name) end - module All - extend ActiveSupport::Concern - include Renderers + def self._render_with_renderer_method_name(key) + "_render_with_renderer_#{key}" + end - included do - self._renderers = RENDERERS + module ClassMethods + + # Adds, by name, a renderer or renderers to the +_renderers+ available + # to call within controller actions. + # + # It is useful when rendering from an <tt>ActionController::Metal</tt> controller or + # otherwise to add an available renderer proc to a specific controller. + # + # Both <tt>ActionController::Base</tt> and <tt>ActionController::API</tt> + # include <tt>ActionController::Renderers::All</tt>, making all renderers + # avaialable in the controller. See <tt>Renderers::RENDERERS</tt> and <tt>Renderers.add</tt>. + # + # Since <tt>ActionController::Metal</tt> controllers cannot render, the controller + # must include <tt>AbstractController::Rendering</tt>, <tt>ActionController::Rendering</tt>, + # and <tt>ActionController::Renderers</tt>, and have at lest one renderer. + # + # Rather than including <tt>ActionController::Renderers::All</tt> and including all renderers, + # you may specify which renderers to include by passing the renderer name or names to + # +use_renderers+. For example, a controller that includes only the <tt>:json</tt> renderer + # (+_render_with_renderer_json+) might look like: + # + # class MetalRenderingController < ActionController::Metal + # include AbstractController::Rendering + # include ActionController::Rendering + # include ActionController::Renderers + # + # use_renderers :json + # + # def show + # render json: record + # end + # end + # + # You must specify a +use_renderer+, else the +controller.renderer+ and + # +controller._renderers+ will be <tt>nil</tt>, and the action will fail. + def use_renderers(*args) + renderers = _renderers + args + self._renderers = renderers.freeze end + alias use_renderer use_renderers + end + + # Called by +render+ in <tt>AbstractController::Rendering</tt> + # which sets the return value as the +response_body+. + # + # If no renderer is found, +super+ returns control to + # <tt>ActionView::Rendering.render_to_body</tt>, if present. + def render_to_body(options) + _render_to_body_with_renderer(options) || super + end + + def _render_to_body_with_renderer(options) + _renderers.each do |name| + if options.key?(name) + _process_options(options) + method_name = Renderers._render_with_renderer_method_name(name) + return send(method_name, options.delete(name), options) + end + end + nil end add :json do |json, options| diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index 26c4550f89..91b3403ad5 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -81,6 +81,10 @@ module ActionController #:nodoc: config_accessor :forgery_protection_origin_check self.forgery_protection_origin_check = false + # Controls whether form-action/method specific CSRF tokens are used. + config_accessor :per_form_csrf_tokens + self.per_form_csrf_tokens = false + helper_method :form_authenticity_token helper_method :protect_against_forgery? end @@ -277,16 +281,25 @@ module ActionController #:nodoc: end # Sets the token value for the current session. - def form_authenticity_token - masked_authenticity_token(session) + def form_authenticity_token(form_options: {}) + masked_authenticity_token(session, form_options: form_options) end # Creates a masked version of the authenticity token that varies # on each request. The masking is used to mitigate SSL attacks # like BREACH. - def masked_authenticity_token(session) + def masked_authenticity_token(session, form_options: {}) + action, method = form_options.values_at(:action, :method) + + raw_token = if per_form_csrf_tokens && action && method + action_path = normalize_action_path(action) + per_form_csrf_token(session, action_path, method) + else + real_csrf_token(session) + end + one_time_pad = SecureRandom.random_bytes(AUTHENTICITY_TOKEN_LENGTH) - encrypted_csrf_token = xor_byte_strings(one_time_pad, real_csrf_token(session)) + encrypted_csrf_token = xor_byte_strings(one_time_pad, raw_token) masked_token = one_time_pad + encrypted_csrf_token Base64.strict_encode64(masked_token) end @@ -316,28 +329,54 @@ module ActionController #:nodoc: compare_with_real_token masked_token, session elsif masked_token.length == AUTHENTICITY_TOKEN_LENGTH * 2 - # Split the token into the one-time pad and the encrypted - # value and decrypt it - one_time_pad = masked_token[0...AUTHENTICITY_TOKEN_LENGTH] - encrypted_csrf_token = masked_token[AUTHENTICITY_TOKEN_LENGTH..-1] - csrf_token = xor_byte_strings(one_time_pad, encrypted_csrf_token) - - compare_with_real_token csrf_token, session + csrf_token = unmask_token(masked_token) + compare_with_real_token(csrf_token, session) || + valid_per_form_csrf_token?(csrf_token, session) else false # Token is malformed end end + def unmask_token(masked_token) + # Split the token into the one-time pad and the encrypted + # value and decrypt it + one_time_pad = masked_token[0...AUTHENTICITY_TOKEN_LENGTH] + encrypted_csrf_token = masked_token[AUTHENTICITY_TOKEN_LENGTH..-1] + xor_byte_strings(one_time_pad, encrypted_csrf_token) + end + def compare_with_real_token(token, session) ActiveSupport::SecurityUtils.secure_compare(token, real_csrf_token(session)) end + def valid_per_form_csrf_token?(token, session) + if per_form_csrf_tokens + correct_token = per_form_csrf_token( + session, + normalize_action_path(request.fullpath), + request.request_method + ) + + ActiveSupport::SecurityUtils.secure_compare(token, correct_token) + else + false + end + end + def real_csrf_token(session) session[:_csrf_token] ||= SecureRandom.base64(AUTHENTICITY_TOKEN_LENGTH) Base64.strict_decode64(session[:_csrf_token]) end + def per_form_csrf_token(session, action_path, method) + OpenSSL::HMAC.digest( + OpenSSL::Digest::SHA256.new, + real_csrf_token(session), + [action_path, method.downcase].join("#") + ) + end + def xor_byte_strings(s1, s2) s1.bytes.zip(s2.bytes).map { |(c1,c2)| c1 ^ c2 }.pack('c*') end @@ -362,5 +401,9 @@ module ActionController #:nodoc: true end end + + def normalize_action_path(action_path) + action_path.split('?').first.to_s.chomp('/') + end end end diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index 957aa746c0..4cd67a85cc 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -109,7 +109,7 @@ module ActionController cattr_accessor :permit_all_parameters, instance_accessor: false cattr_accessor :action_on_unpermitted_parameters, instance_accessor: false - delegate :keys, :key?, :has_key?, :empty?, :inspect, to: :@parameters + delegate :keys, :key?, :has_key?, :empty?, :include?, :inspect, to: :@parameters # By default, never raise an UnpermittedParameters exception if these # params are present. The default includes both 'controller' and 'action' @@ -176,7 +176,7 @@ module ActionController # safe_params.to_h # => {"name"=>"Senjougahara Hitagi"} def to_h if permitted? - convert_parameters_to_hashes(@parameters) + convert_parameters_to_hashes(@parameters, :to_h) else slice(*self.class.always_permitted_parameters).permit!.to_h end @@ -186,7 +186,7 @@ module ActionController # <tt>ActiveSupport::HashWithIndifferentAccess</tt> representation of this # parameter. def to_unsafe_h - convert_parameters_to_hashes(@parameters) + convert_parameters_to_hashes(@parameters, :to_unsafe_h) end alias_method :to_unsafe_hash, :to_unsafe_h @@ -595,16 +595,16 @@ module ActionController end end - def convert_parameters_to_hashes(value) + def convert_parameters_to_hashes(value, using) case value when Array - value.map { |v| convert_parameters_to_hashes(v) } + value.map { |v| convert_parameters_to_hashes(v, using) } when Hash value.transform_values do |v| - convert_parameters_to_hashes(v) + convert_parameters_to_hashes(v, using) end.with_indifferent_access when Parameters - value.to_h + value.send(using) else value end diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb index f6336c8c7a..7bc6575ccd 100644 --- a/actionpack/lib/action_dispatch.rb +++ b/actionpack/lib/action_dispatch.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2015 David Heinemeier Hansson +# Copyright (c) 2004-2016 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb index 0152c17ed4..e9b25339dc 100644 --- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb +++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb @@ -67,10 +67,10 @@ module ActionDispatch v = if params_readable Array(Mime[parameters[:format]]) - elsif format = format_from_path_extension - Array(Mime[format]) elsif use_accept_header && valid_accept_header accepts + elsif extension_format = format_from_path_extension + [extension_format] elsif xhr? [Mime[:js]] else @@ -166,7 +166,7 @@ module ActionDispatch def format_from_path_extension path = @env['action_dispatch.original_path'] || @env['PATH_INFO'] if match = path && path.match(/\.(\w+)\z/) - match.captures.first + Mime[match.captures.first] end end end diff --git a/actionpack/lib/action_dispatch/journey/path/pattern.rb b/actionpack/lib/action_dispatch/journey/path/pattern.rb index 5ee8810066..018b89a2b7 100644 --- a/actionpack/lib/action_dispatch/journey/path/pattern.rb +++ b/actionpack/lib/action_dispatch/journey/path/pattern.rb @@ -124,7 +124,7 @@ module ActionDispatch end def captures - (length - 1).times.map { |i| self[i + 1] } + Array.new(length - 1) { |i| self[i + 1] } end def [](x) diff --git a/actionpack/lib/action_dispatch/middleware/ssl.rb b/actionpack/lib/action_dispatch/middleware/ssl.rb index 8f8f1bab8b..735b5939dd 100644 --- a/actionpack/lib/action_dispatch/middleware/ssl.rb +++ b/actionpack/lib/action_dispatch/middleware/ssl.rb @@ -4,16 +4,18 @@ module ActionDispatch # requests: # # 1. TLS redirect: Permanently redirects http:// requests to https:// - # with the same URL host, path, etc. This is always enabled. Set - # `config.ssl_options` to modify the destination URL - # (e.g. `redirect: { host: "secure.widgets.com", port: 8080 }`) + # with the same URL host, path, etc. Enabled by default. Set `config.ssl_options` + # to modify the destination URL + # (e.g. `redirect: { host: "secure.widgets.com", port: 8080 }`), or set + # `redirect: false` to disable this feature. # # 2. Secure cookies: Sets the `secure` flag on cookies to tell browsers they - # mustn't be sent along with http:// requests. This is always enabled. + # mustn't be sent along with http:// requests. Enabled by default. Set + # `config.ssl_options` with `secure_cookies: false` to disable this feature. # # 3. HTTP Strict Transport Security (HSTS): Tells the browser to remember # this site as TLS-only and automatically redirect non-TLS requests. - # Enabled by default. Pass `hsts: false` to disable. + # Enabled by default. Configure `config.ssl_options` with `hsts: false` to disable. # # Set `config.ssl_options` with `hsts: { … }` to configure HSTS: # * `expires`: How long, in seconds, these settings will stick. Defaults to @@ -41,7 +43,7 @@ module ActionDispatch { expires: HSTS_EXPIRES_IN, subdomains: false, preload: false } end - def initialize(app, redirect: {}, hsts: {}, **options) + def initialize(app, redirect: {}, hsts: {}, secure_cookies: true, **options) @app = app if options[:host] || options[:port] @@ -54,6 +56,7 @@ module ActionDispatch @redirect = redirect end + @secure_cookies = secure_cookies @hsts_header = build_hsts_header(normalize_hsts_options(hsts)) end @@ -63,10 +66,11 @@ module ActionDispatch if request.ssl? @app.call(env).tap do |status, headers, body| set_hsts_header! headers - flag_cookies_as_secure! headers + flag_cookies_as_secure! headers if @secure_cookies end else - redirect_to_https request + return redirect_to_https request if @redirect + @app.call(env) end end diff --git a/actionpack/lib/action_dispatch/routing/inspector.rb b/actionpack/lib/action_dispatch/routing/inspector.rb index f3a5268d2e..69e6dd5215 100644 --- a/actionpack/lib/action_dispatch/routing/inspector.rb +++ b/actionpack/lib/action_dispatch/routing/inspector.rb @@ -65,7 +65,7 @@ module ActionDispatch routes = collect_routes(routes_to_display) if routes.none? - formatter.no_routes + formatter.no_routes(collect_routes(@routes), filter) return formatter.result end @@ -84,7 +84,8 @@ module ActionDispatch def filter_routes(filter) if filter - @routes.select { |route| route.defaults[:controller] == filter } + filter_name = filter.underscore.sub(/_controller$/, '') + @routes.select { |route| route.defaults[:controller] == filter_name } else @routes end @@ -136,17 +137,27 @@ module ActionDispatch @buffer << draw_header(routes) end - def no_routes - @buffer << <<-MESSAGE.strip_heredoc + def no_routes(routes, filter) + @buffer << + if routes.none? + <<-MESSAGE.strip_heredoc You don't have any routes defined! Please add some routes in config/routes.rb. - - For more information about routes, see the Rails guide: http://guides.rubyonrails.org/routing.html. MESSAGE + elsif missing_controller?(filter) + "The controller #{filter} does not exist!" + else + "No routes were found for this controller" + end + @buffer << "For more information about routes, see the Rails guide: http://guides.rubyonrails.org/routing.html." end private + def missing_controller?(controller_name) + [ controller_name.camelize, "#{controller_name.camelize}Controller" ].none?(&:safe_constantize) + end + def draw_section(routes) header_lengths = ['Prefix', 'Verb', 'URI Pattern'].map(&:length) name_width, verb_width, path_width = widths(routes).zip(header_lengths).map(&:max) @@ -187,7 +198,7 @@ module ActionDispatch def header(routes) end - def no_routes + def no_routes(*) @buffer << <<-MESSAGE.strip_heredoc <p>You don't have any routes defined!</p> <ul> diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 18cd205bad..522012063d 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -387,24 +387,6 @@ module ActionDispatch end module Base - # You can specify what Rails should route "/" to with the root method: - # - # root to: 'pages#main' - # - # For options, see +match+, as +root+ uses it internally. - # - # You can also pass a string which will expand - # - # root 'pages#main' - # - # You should put the root route at the top of <tt>config/routes.rb</tt>, - # because this means it will be matched first. As this is the most popular route - # of most Rails applications, this is beneficial. - def root(options = {}) - name = has_named_route?(:root) ? nil : :root - match '/', { as: name, via: :get }.merge!(options) - end - # Matches a url pattern to one or more routes. # # You should not use the +match+ method in your router @@ -1689,7 +1671,20 @@ to this: @set.add_route(mapping, ast, as, anchor) end - def root(path, options={}) + # You can specify what Rails should route "/" to with the root method: + # + # root to: 'pages#main' + # + # For options, see +match+, as +root+ uses it internally. + # + # You can also pass a string which will expand + # + # root 'pages#main' + # + # You should put the root route at the top of <tt>config/routes.rb</tt>, + # because this means it will be matched first. As this is the most popular route + # of most Rails applications, this is beneficial. + def root(path, options = {}) if path.is_a?(String) options[:to] = path elsif path.is_a?(Hash) and options.empty? @@ -1701,11 +1696,11 @@ to this: if @scope.resources? with_scope_level(:root) do path_scope(parent_resource.path) do - super(options) + match_root_route(options) end end else - super(options) + match_root_route(options) end end @@ -1900,6 +1895,11 @@ to this: ensure @scope = @scope.parent end + + def match_root_route(options) + name = has_named_route?(:root) ? nil : :root + match '/', { :as => name, :via => :get }.merge!(options) + end end # Routing Concerns allow you to declare common routes that can be reused diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 2bd2e53252..846b5fa1fc 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -281,8 +281,17 @@ module ActionDispatch helper = UrlHelper.create(route, opts, route_key, url_strategy) mod.module_eval do define_method(name) do |*args| - options = nil - options = args.pop if args.last.is_a? Hash + last = args.last + options = case last + when Hash + args.pop + when ActionController::Parameters + if last.permitted? + args.pop.to_h + else + raise ArgumentError, "Generating an URL from non sanitized request parameters is insecure!" + end + end helper.call self, args, options end end diff --git a/actionpack/lib/action_pack.rb b/actionpack/lib/action_pack.rb index f664dab620..941877d10d 100644 --- a/actionpack/lib/action_pack.rb +++ b/actionpack/lib/action_pack.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2015 David Heinemeier Hansson +# Copyright (c) 2004-2016 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb index ef7aab72c6..604ba267d6 100644 --- a/actionpack/test/abstract_unit.rb +++ b/actionpack/test/abstract_unit.rb @@ -371,6 +371,12 @@ module RoutingTestHelpers end end +class MetalRenderingController < ActionController::Metal + include AbstractController::Rendering + include ActionController::Rendering + include ActionController::Renderers +end + class ResourcesController < ActionController::Base def index() head :ok end alias_method :show, :index diff --git a/actionpack/test/controller/http_basic_authentication_test.rb b/actionpack/test/controller/http_basic_authentication_test.rb index 0a5e5402b9..adcf259317 100644 --- a/actionpack/test/controller/http_basic_authentication_test.rb +++ b/actionpack/test/controller/http_basic_authentication_test.rb @@ -5,6 +5,7 @@ class HttpBasicAuthenticationTest < ActionController::TestCase before_action :authenticate, only: :index before_action :authenticate_with_request, only: :display before_action :authenticate_long_credentials, only: :show + before_action :auth_with_special_chars, only: :special_creds http_basic_authenticate_with :name => "David", :password => "Goliath", :only => :search @@ -20,6 +21,10 @@ class HttpBasicAuthenticationTest < ActionController::TestCase render plain: 'Only for loooooong credentials' end + def special_creds + render plain: 'Only for special credentials' + end + def search render plain: 'All inline' end @@ -40,6 +45,12 @@ class HttpBasicAuthenticationTest < ActionController::TestCase end end + def auth_with_special_chars + authenticate_or_request_with_http_basic do |username, password| + username == 'login!@#$%^&*()_+{}[];"\',./<>?`~ \n\r\t' && password == 'pwd:!@#$%^&*()_+{}[];"\',./<>?`~ \n\r\t' + end + end + def authenticate_long_credentials authenticate_or_request_with_http_basic do |username, password| username == '1234567890123456789012345678901234567890' && password == '1234567890123456789012345678901234567890' @@ -100,7 +111,7 @@ class HttpBasicAuthenticationTest < ActionController::TestCase assert_no_match(/\n/, result) end - test "succesful authentication with uppercase authorization scheme" do + test "successful authentication with uppercase authorization scheme" do @request.env['HTTP_AUTHORIZATION'] = "BASIC #{::Base64.encode64("lifo:world")}" get :index @@ -133,6 +144,14 @@ class HttpBasicAuthenticationTest < ActionController::TestCase assert_equal 'Definitely Maybe', @response.body end + test "authentication request with valid credential special chars" do + @request.env['HTTP_AUTHORIZATION'] = encode_credentials('login!@#$%^&*()_+{}[];"\',./<>?`~ \n\r\t', 'pwd:!@#$%^&*()_+{}[];"\',./<>?`~ \n\r\t') + get :special_creds + + assert_response :success + assert_equal 'Only for special credentials', @response.body + end + test "authenticate with class method" do @request.env['HTTP_AUTHORIZATION'] = encode_credentials('David', 'Goliath') get :search diff --git a/actionpack/test/controller/metal/renderers_test.rb b/actionpack/test/controller/metal/renderers_test.rb new file mode 100644 index 0000000000..007866a559 --- /dev/null +++ b/actionpack/test/controller/metal/renderers_test.rb @@ -0,0 +1,42 @@ +require 'abstract_unit' +require 'active_support/core_ext/hash/conversions' + +class MetalRenderingJsonController < MetalRenderingController + class Model + def to_json(options = {}) + { a: 'b' }.to_json(options) + end + + def to_xml(options = {}) + { a: 'b' }.to_xml(options) + end + end + + use_renderers :json + + def one + render json: Model.new + end + + def two + render xml: Model.new + end +end + +class RenderersMetalTest < ActionController::TestCase + tests MetalRenderingJsonController + + def test_render_json + get :one + assert_response :success + assert_equal({ a: 'b' }.to_json, @response.body) + assert_equal 'application/json', @response.content_type + end + + def test_render_xml + get :two + assert_response :success + assert_equal(" ", @response.body) + assert_equal 'text/plain', @response.content_type + end +end diff --git a/actionpack/test/controller/parameters/parameters_permit_test.rb b/actionpack/test/controller/parameters/parameters_permit_test.rb index f23aa599c1..896bda2597 100644 --- a/actionpack/test/controller/parameters/parameters_permit_test.rb +++ b/actionpack/test/controller/parameters/parameters_permit_test.rb @@ -294,8 +294,16 @@ class ParametersPermitTest < ActiveSupport::TestCase end test "to_unsafe_h returns unfiltered params" do - assert @params.to_h.is_a? ActiveSupport::HashWithIndifferentAccess - assert_not @params.to_h.is_a? ActionController::Parameters + assert @params.to_unsafe_h.is_a? ActiveSupport::HashWithIndifferentAccess + assert_not @params.to_unsafe_h.is_a? ActionController::Parameters + end + + test "to_unsafe_h returns unfiltered params even after accessing few keys" do + params = ActionController::Parameters.new("f"=>{"language_facet"=>["Tibetan"]}) + expected = {"f"=>{"language_facet"=>["Tibetan"]}} + + assert params['f'].is_a? ActionController::Parameters + assert_equal expected, params.to_unsafe_h end test "to_h only deep dups Ruby collections" do @@ -325,4 +333,10 @@ class ParametersPermitTest < ActiveSupport::TestCase assert_equal({ 'companies' => [ company, :acme ] }, params.to_unsafe_h) assert_not company.dupped end + + test "included? returns true when the key is present" do + assert @params.include? :person + assert @params.include? 'person' + assert_not @params.include? :gorilla + end end diff --git a/actionpack/test/controller/render_other_test.rb b/actionpack/test/controller/render_other_test.rb deleted file mode 100644 index 1f5215ac55..0000000000 --- a/actionpack/test/controller/render_other_test.rb +++ /dev/null @@ -1,24 +0,0 @@ -require 'abstract_unit' - - -class RenderOtherTest < ActionController::TestCase - class TestController < ActionController::Base - def render_simon_says - render :simon => "foo" - end - end - - tests TestController - - def test_using_custom_render_option - ActionController.add_renderer :simon do |says, options| - self.content_type = Mime[:text] - self.response_body = "Simon says: #{says}" - end - - get :render_simon_says - assert_equal "Simon says: foo", @response.body - ensure - ActionController.remove_renderer :simon - end -end diff --git a/actionpack/test/controller/renderers_test.rb b/actionpack/test/controller/renderers_test.rb new file mode 100644 index 0000000000..e6c2e4636e --- /dev/null +++ b/actionpack/test/controller/renderers_test.rb @@ -0,0 +1,90 @@ +require 'abstract_unit' +require 'controller/fake_models' +require 'active_support/logger' + +class RenderersTest < ActionController::TestCase + class XmlRenderable + def to_xml(options) + options[:root] ||= "i-am-xml" + "<#{options[:root]}/>" + end + end + class JsonRenderable + def as_json(options={}) + hash = { :a => :b, :c => :d, :e => :f } + hash.except!(*options[:except]) if options[:except] + hash + end + + def to_json(options = {}) + super :except => [:c, :e] + end + end + class CsvRenderable + def to_csv + "c,s,v" + end + end + class TestController < ActionController::Base + + def render_simon_says + render :simon => "foo" + end + + def respond_to_mime + respond_to do |type| + type.json do + render json: JsonRenderable.new + end + type.js { render json: 'JS', callback: 'alert' } + type.csv { render csv: CsvRenderable.new } + type.xml { render xml: XmlRenderable.new } + type.html { render body: "HTML" } + type.rss { render body: "RSS" } + type.all { render body: "Nothing" } + type.any(:js, :xml) { render body: "Either JS or XML" } + end + end + end + + tests TestController + + def setup + # enable a logger so that (e.g.) the benchmarking stuff runs, so we can get + # a more accurate simulation of what happens in "real life". + super + @controller.logger = ActiveSupport::Logger.new(nil) + end + + def test_using_custom_render_option + ActionController.add_renderer :simon do |says, options| + self.content_type = Mime[:text] + self.response_body = "Simon says: #{says}" + end + + get :render_simon_says + assert_equal "Simon says: foo", @response.body + ensure + ActionController.remove_renderer :simon + end + + def test_raises_missing_template_no_renderer + assert_raise ActionView::MissingTemplate do + get :respond_to_mime, format: 'csv' + end + assert_equal Mime[:csv], @response.content_type + assert_equal "", @response.body + end + + def test_adding_csv_rendering_via_renderers_add + ActionController::Renderers.add :csv do |value, options| + send_data value.to_csv, type: Mime[:csv] + end + @request.accept = "text/csv" + get :respond_to_mime, format: 'csv' + assert_equal Mime[:csv], @response.content_type + assert_equal "c,s,v", @response.body + ensure + ActionController::Renderers.remove :csv + end +end diff --git a/actionpack/test/controller/request_forgery_protection_test.rb b/actionpack/test/controller/request_forgery_protection_test.rb index 87a8ed3dc9..1984ad8825 100644 --- a/actionpack/test/controller/request_forgery_protection_test.rb +++ b/actionpack/test/controller/request_forgery_protection_test.rb @@ -128,6 +128,23 @@ class CustomAuthenticityParamController < RequestForgeryProtectionControllerUsin end end +class PerFormTokensController < ActionController::Base + protect_from_forgery :with => :exception + self.per_form_csrf_tokens = true + + def index + render inline: "<%= form_tag (params[:form_path] || '/per_form_tokens/post_one'), method: (params[:form_method] || :post) %>" + end + + def post_one + render plain: '' + end + + def post_two + render plain: '' + end +end + # common test methods module RequestForgeryProtectionTests def setup @@ -623,3 +640,158 @@ class CustomAuthenticityParamControllerTest < ActionController::TestCase end end end + +class PerFormTokensControllerTest < ActionController::TestCase + def test_per_form_token_is_same_size_as_global_token + get :index + expected = ActionController::RequestForgeryProtection::AUTHENTICITY_TOKEN_LENGTH + actual = @controller.send(:per_form_csrf_token, session, '/path', 'post').size + assert_equal expected, actual + end + + def test_accepts_token_for_correct_path_and_method + get :index + + form_token = nil + assert_select 'input[name=custom_authenticity_token]' do |elts| + form_token = elts.first['value'] + assert_not_nil form_token + end + + actual = @controller.send(:unmask_token, Base64.strict_decode64(form_token)) + expected = @controller.send(:per_form_csrf_token, session, '/per_form_tokens/post_one', 'post') + assert_equal expected, actual + + # This is required because PATH_INFO isn't reset between requests. + @request.env['PATH_INFO'] = '/per_form_tokens/post_one' + assert_nothing_raised do + post :post_one, params: {custom_authenticity_token: form_token} + end + assert_response :success + end + + def test_rejects_token_for_incorrect_path + get :index + + form_token = nil + assert_select 'input[name=custom_authenticity_token]' do |elts| + form_token = elts.first['value'] + assert_not_nil form_token + end + + actual = @controller.send(:unmask_token, Base64.strict_decode64(form_token)) + expected = @controller.send(:per_form_csrf_token, session, '/per_form_tokens/post_one', 'post') + assert_equal expected, actual + + # This is required because PATH_INFO isn't reset between requests. + @request.env['PATH_INFO'] = '/per_form_tokens/post_two' + assert_raises(ActionController::InvalidAuthenticityToken) do + post :post_two, params: {custom_authenticity_token: form_token} + end + end + + def test_rejects_token_for_incorrect_method + get :index + + form_token = nil + assert_select 'input[name=custom_authenticity_token]' do |elts| + form_token = elts.first['value'] + assert_not_nil form_token + end + + actual = @controller.send(:unmask_token, Base64.strict_decode64(form_token)) + expected = @controller.send(:per_form_csrf_token, session, '/per_form_tokens/post_one', 'post') + assert_equal expected, actual + + # This is required because PATH_INFO isn't reset between requests. + @request.env['PATH_INFO'] = '/per_form_tokens/post_one' + assert_raises(ActionController::InvalidAuthenticityToken) do + patch :post_one, params: {custom_authenticity_token: form_token} + end + end + + def test_accepts_global_csrf_token + get :index + + token = @controller.send(:form_authenticity_token) + + # This is required because PATH_INFO isn't reset between requests. + @request.env['PATH_INFO'] = '/per_form_tokens/post_one' + assert_nothing_raised do + post :post_one, params: {custom_authenticity_token: token} + end + assert_response :success + end + + def test_ignores_params + get :index, params: {form_path: '/per_form_tokens/post_one?foo=bar'} + + form_token = nil + assert_select 'input[name=custom_authenticity_token]' do |elts| + form_token = elts.first['value'] + assert_not_nil form_token + end + + actual = @controller.send(:unmask_token, Base64.strict_decode64(form_token)) + expected = @controller.send(:per_form_csrf_token, session, '/per_form_tokens/post_one', 'post') + assert_equal expected, actual + + # This is required because PATH_INFO isn't reset between requests. + @request.env['PATH_INFO'] = '/per_form_tokens/post_one?foo=baz' + assert_nothing_raised do + post :post_one, params: {custom_authenticity_token: form_token, baz: 'foo'} + end + assert_response :success + end + + def test_ignores_trailing_slash_during_generation + get :index, params: {form_path: '/per_form_tokens/post_one/'} + + form_token = nil + assert_select 'input[name=custom_authenticity_token]' do |elts| + form_token = elts.first['value'] + assert_not_nil form_token + end + + # This is required because PATH_INFO isn't reset between requests. + @request.env['PATH_INFO'] = '/per_form_tokens/post_one' + assert_nothing_raised do + post :post_one, params: {custom_authenticity_token: form_token} + end + assert_response :success + end + + def test_ignores_trailing_slash_during_validation + get :index + + form_token = nil + assert_select 'input[name=custom_authenticity_token]' do |elts| + form_token = elts.first['value'] + assert_not_nil form_token + end + + # This is required because PATH_INFO isn't reset between requests. + @request.env['PATH_INFO'] = '/per_form_tokens/post_one/' + assert_nothing_raised do + post :post_one, params: {custom_authenticity_token: form_token} + end + assert_response :success + end + + def test_method_is_case_insensitive + get :index, params: {form_method: "POST"} + + form_token = nil + assert_select 'input[name=custom_authenticity_token]' do |elts| + form_token = elts.first['value'] + assert_not_nil form_token + end + + # This is required because PATH_INFO isn't reset between requests. + @request.env['PATH_INFO'] = '/per_form_tokens/post_one/' + assert_nothing_raised do + post :post_one, params: {custom_authenticity_token: form_token} + end + assert_response :success + end +end diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb index 7dd9d05e62..0edad72fd9 100644 --- a/actionpack/test/dispatch/request_test.rb +++ b/actionpack/test/dispatch/request_test.rb @@ -897,6 +897,27 @@ class RequestFormat < BaseRequestTest ActionDispatch::Request.ignore_accept_header = old_ignore_accept_header end end + + test "format taken from the path extension" do + request = stub_request 'PATH_INFO' => '/foo.xml' + assert_called(request, :parameters, times: 1, returns: {}) do + assert_equal [Mime[:xml]], request.formats + end + + request = stub_request 'PATH_INFO' => '/foo.123' + assert_called(request, :parameters, times: 1, returns: {}) do + assert_equal [Mime[:html]], request.formats + end + end + + test "formats from accept headers have higher precedence than path extension" do + request = stub_request 'HTTP_ACCEPT' => 'application/json', + 'PATH_INFO' => '/foo.xml' + + assert_called(request, :parameters, times: 1, returns: {}) do + assert_equal [Mime[:json]], request.formats + end + end end class RequestMimeType < BaseRequestTest diff --git a/actionpack/test/dispatch/routing/inspector_test.rb b/actionpack/test/dispatch/routing/inspector_test.rb index a17d07c40b..7382c267c7 100644 --- a/actionpack/test/dispatch/routing/inspector_test.rb +++ b/actionpack/test/dispatch/routing/inspector_test.rb @@ -7,6 +7,9 @@ class MountedRackApp end end +class Rails::DummyController +end + module ActionDispatch module Routing class RoutesInspectorTest < ActiveSupport::TestCase @@ -331,6 +334,41 @@ module ActionDispatch " cart GET /cart(.:format) cart#show" ], output end + + def test_routes_with_undefined_filter + output = draw(:filter => 'Rails::MissingController') do + get 'photos/:id' => 'photos#show', :id => /[A-Z]\d{5}/ + end + + assert_equal [ + "The controller Rails::MissingController does not exist!", + "For more information about routes, see the Rails guide: http://guides.rubyonrails.org/routing.html." + ], output + end + + def test_no_routes_matched_filter + output = draw(:filter => 'rails/dummy') do + get 'photos/:id' => 'photos#show', :id => /[A-Z]\d{5}/ + end + + assert_equal [ + "No routes were found for this controller", + "For more information about routes, see the Rails guide: http://guides.rubyonrails.org/routing.html." + ], output + end + + def test_no_routes_were_defined + output = draw(:filter => 'Rails::DummyController') { } + + assert_equal [ + "You don't have any routes defined!", + "", + "Please add some routes in config/routes.rb.", + "", + "For more information about routes, see the Rails guide: http://guides.rubyonrails.org/routing.html." + ], output + end + end end end diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index 82222a141c..62d65ec5c0 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -3578,6 +3578,27 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal 'HEAD', @response.body end + def test_passing_action_parameters_to_url_helpers_raises_error_if_parameters_are_not_permitted + draw do + root :to => 'projects#index' + end + params = ActionController::Parameters.new(id: '1') + + assert_raises ArgumentError do + root_path(params) + end + end + + def test_passing_action_parameters_to_url_helpers_is_allowed_if_parameters_are_permitted + draw do + root :to => 'projects#index' + end + params = ActionController::Parameters.new(id: '1') + params.permit! + + assert_equal '/?id=1', root_path(params) + end + private def draw(&block) diff --git a/actionpack/test/dispatch/ssl_test.rb b/actionpack/test/dispatch/ssl_test.rb index 7a5b8393dc..c66a0e6a7a 100644 --- a/actionpack/test/dispatch/ssl_test.rb +++ b/actionpack/test/dispatch/ssl_test.rb @@ -12,25 +12,31 @@ class SSLTest < ActionDispatch::IntegrationTest end class RedirectSSLTest < SSLTest - def assert_not_redirected(url, headers: {}) - self.app = build_app + + def assert_not_redirected(url, headers: {}, redirect: {}, deprecated_host: nil, + deprecated_port: nil) + + self.app = build_app ssl_options: { redirect: redirect, + host: deprecated_host, port: deprecated_port + } + get url, headers: headers assert_response :ok end - def assert_redirected(host: nil, port: nil, status: 301, body: [], - deprecated_host: nil, deprecated_port: nil, + def assert_redirected(redirect: {}, deprecated_host: nil, deprecated_port: nil, from: 'http://a/b?c=d', to: from.sub('http', 'https')) - self.app = build_app ssl_options: { - redirect: { host: host, port: port, status: status, body: body }, + redirect = { status: 301, body: [] }.merge(redirect) + + self.app = build_app ssl_options: { redirect: redirect, host: deprecated_host, port: deprecated_port } get from - assert_response status + assert_response redirect[:status] || 301 assert_redirected_to to - assert_equal body.join, @response.body + assert_equal redirect[:body].join, @response.body end test 'https is not redirected' do @@ -46,31 +52,31 @@ class RedirectSSLTest < SSLTest end test 'redirect with non-301 status' do - assert_redirected status: 307 + assert_redirected redirect: { status: 307 } end test 'redirect with custom body' do - assert_redirected body: ['foo'] + assert_redirected redirect: { body: ['foo'] } end test 'redirect to specific host' do - assert_redirected host: 'ssl', to: 'https://ssl/b?c=d' + assert_redirected redirect: { host: 'ssl' }, to: 'https://ssl/b?c=d' end test 'redirect to default port' do - assert_redirected port: 443 + assert_redirected redirect: { port: 443 } end test 'redirect to non-default port' do - assert_redirected port: 8443, to: 'https://a:8443/b?c=d' + assert_redirected redirect: { port: 8443 }, to: 'https://a:8443/b?c=d' end test 'redirect to different host and non-default port' do - assert_redirected host: 'ssl', port: 8443, to: 'https://ssl:8443/b?c=d' + assert_redirected redirect: { host: 'ssl', port: 8443 }, to: 'https://ssl:8443/b?c=d' end test 'redirect to different host including port' do - assert_redirected host: 'ssl:443', to: 'https://ssl:443/b?c=d' + assert_redirected redirect: { host: 'ssl:443' }, to: 'https://ssl:443/b?c=d' end test ':host is deprecated, moved within redirect: { host: … }' do @@ -84,6 +90,10 @@ class RedirectSSLTest < SSLTest assert_redirected deprecated_port: 1, to: 'https://a:1/b?c=d' end end + + test 'no redirect with redirect set to false' do + assert_not_redirected 'http://example.org', redirect: false + end end class StrictTransportSecurityTest < SSLTest @@ -187,6 +197,11 @@ class SecureCookiesTest < SSLTest assert_cookies 'problem=def; path=/; Secure; HttpOnly' end + def test_cookies_as_not_secure_with_secure_cookies_disabled + get headers: { 'Set-Cookie' => DEFAULT }, ssl_options: { secure_cookies: false } + assert_cookies(*DEFAULT.split("\n")) + end + def test_no_cookies get assert_nil response.headers['Set-Cookie'] |