mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
6520ea5f7e
Allowing :controller and :action values to be specified via the path in config/routes.rb has been an underlying cause of a number of issues in Rails that have resulted in security releases. In light of this it's better that controllers and actions are explicitly whitelisted rather than trying to blacklist or sanitize 'bad' values.
533 lines
16 KiB
Ruby
533 lines
16 KiB
Ruby
require 'abstract_unit'
|
|
|
|
module ActionDispatch
|
|
module Journey
|
|
class TestRouter < ActiveSupport::TestCase
|
|
attr_reader :mapper, :routes, :route_set, :router
|
|
|
|
def setup
|
|
@app = Routing::RouteSet::Dispatcher.new({})
|
|
@route_set = ActionDispatch::Routing::RouteSet.new
|
|
@routes = @route_set.router.routes
|
|
@router = @route_set.router
|
|
@formatter = @route_set.formatter
|
|
@mapper = ActionDispatch::Routing::Mapper.new @route_set
|
|
end
|
|
|
|
def test_dashes
|
|
get '/foo-bar-baz', to: 'foo#bar'
|
|
|
|
env = rails_env 'PATH_INFO' => '/foo-bar-baz'
|
|
called = false
|
|
router.recognize(env) do |r, params|
|
|
called = true
|
|
end
|
|
assert called
|
|
end
|
|
|
|
def test_unicode
|
|
get '/ほげ', to: 'foo#bar'
|
|
|
|
#match the escaped version of /ほげ
|
|
env = rails_env 'PATH_INFO' => '/%E3%81%BB%E3%81%92'
|
|
called = false
|
|
router.recognize(env) do |r, params|
|
|
called = true
|
|
end
|
|
assert called
|
|
end
|
|
|
|
def test_regexp_first_precedence
|
|
get "/whois/:domain", :domain => /\w+\.[\w\.]+/, to: "foo#bar"
|
|
get "/whois/:id(.:format)", to: "foo#baz"
|
|
|
|
env = rails_env 'PATH_INFO' => '/whois/example.com'
|
|
|
|
list = []
|
|
router.recognize(env) do |r, params|
|
|
list << r
|
|
end
|
|
assert_equal 2, list.length
|
|
|
|
r = list.first
|
|
|
|
assert_equal '/whois/:domain(.:format)', r.path.spec.to_s
|
|
end
|
|
|
|
def test_required_parts_verified_are_anchored
|
|
get "/foo/:id", :id => /\d/, anchor: false, to: "foo#bar"
|
|
|
|
assert_raises(ActionController::UrlGenerationError) do
|
|
@formatter.generate(nil, { :controller => "foo", :action => "bar", :id => '10' }, { })
|
|
end
|
|
end
|
|
|
|
def test_required_parts_are_verified_when_building
|
|
get "/foo/:id", :id => /\d+/, anchor: false, to: "foo#bar"
|
|
|
|
path, _ = @formatter.generate(nil, { :controller => "foo", :action => "bar", :id => '10' }, { })
|
|
assert_equal '/foo/10', path
|
|
|
|
assert_raises(ActionController::UrlGenerationError) do
|
|
@formatter.generate(nil, { :id => 'aa' }, { })
|
|
end
|
|
end
|
|
|
|
def test_only_required_parts_are_verified
|
|
get "/foo(/:id)", :id => /\d/, :to => "foo#bar"
|
|
|
|
path, _ = @formatter.generate(nil, { :controller => "foo", :action => "bar", :id => '10' }, { })
|
|
assert_equal '/foo/10', path
|
|
|
|
path, _ = @formatter.generate(nil, { :controller => "foo", :action => "bar" }, { })
|
|
assert_equal '/foo', path
|
|
|
|
path, _ = @formatter.generate(nil, { :controller => "foo", :action => "bar", :id => 'aa' }, { })
|
|
assert_equal '/foo/aa', path
|
|
end
|
|
|
|
def test_knows_what_parts_are_missing_from_named_route
|
|
route_name = "gorby_thunderhorse"
|
|
get "/foo/:id", :as => route_name, :id => /\d+/, :to => "foo#bar"
|
|
|
|
error = assert_raises(ActionController::UrlGenerationError) do
|
|
@formatter.generate(route_name, { }, { })
|
|
end
|
|
|
|
assert_match(/missing required keys: \[:id\]/, error.message)
|
|
end
|
|
|
|
def test_does_not_include_missing_keys_message
|
|
route_name = "gorby_thunderhorse"
|
|
|
|
error = assert_raises(ActionController::UrlGenerationError) do
|
|
@formatter.generate(route_name, { }, { })
|
|
end
|
|
|
|
assert_no_match(/missing required keys: \[\]/, error.message)
|
|
end
|
|
|
|
def test_X_Cascade
|
|
get "/messages(.:format)", to: "foo#bar"
|
|
resp = router.serve(rails_env({ 'REQUEST_METHOD' => 'GET', 'PATH_INFO' => '/lol' }))
|
|
assert_equal ['Not Found'], resp.last
|
|
assert_equal 'pass', resp[1]['X-Cascade']
|
|
assert_equal 404, resp.first
|
|
end
|
|
|
|
def test_clear_trailing_slash_from_script_name_on_root_unanchored_routes
|
|
app = lambda { |env| [200, {}, ['success!']] }
|
|
get '/weblog', :to => app
|
|
|
|
env = rack_env('SCRIPT_NAME' => '', 'PATH_INFO' => '/weblog')
|
|
resp = route_set.call env
|
|
assert_equal ['success!'], resp.last
|
|
assert_equal '', env['SCRIPT_NAME']
|
|
end
|
|
|
|
def test_defaults_merge_correctly
|
|
get '/foo(/:id)', to: "foo#bar", id: nil
|
|
|
|
env = rails_env 'PATH_INFO' => '/foo/10'
|
|
router.recognize(env) do |r, params|
|
|
assert_equal({:id => '10', :controller => "foo", :action => "bar"}, params)
|
|
end
|
|
|
|
env = rails_env 'PATH_INFO' => '/foo'
|
|
router.recognize(env) do |r, params|
|
|
assert_equal({:id => nil, :controller => "foo", :action => "bar"}, params)
|
|
end
|
|
end
|
|
|
|
def test_recognize_with_unbound_regexp
|
|
get "/foo", anchor: false, to: "foo#bar"
|
|
|
|
env = rails_env 'PATH_INFO' => '/foo/bar'
|
|
|
|
router.recognize(env) { |*_| }
|
|
|
|
assert_equal '/foo', env.env['SCRIPT_NAME']
|
|
assert_equal '/bar', env.env['PATH_INFO']
|
|
end
|
|
|
|
def test_bound_regexp_keeps_path_info
|
|
get "/foo", to: "foo#bar"
|
|
|
|
env = rails_env 'PATH_INFO' => '/foo'
|
|
|
|
before = env.env['SCRIPT_NAME']
|
|
|
|
router.recognize(env) { |*_| }
|
|
|
|
assert_equal before, env.env['SCRIPT_NAME']
|
|
assert_equal '/foo', env.env['PATH_INFO']
|
|
end
|
|
|
|
def test_path_not_found
|
|
[
|
|
"/messages(.:format)",
|
|
"/messages/new(.:format)",
|
|
"/messages/:id/edit(.:format)",
|
|
"/messages/:id(.:format)"
|
|
].each do |path|
|
|
get path, to: "foo#bar"
|
|
end
|
|
env = rails_env 'PATH_INFO' => '/messages/unknown/path'
|
|
yielded = false
|
|
|
|
router.recognize(env) do |*whatever|
|
|
yielded = true
|
|
end
|
|
assert_not yielded
|
|
end
|
|
|
|
def test_required_part_in_recall
|
|
get "/messages/:a/:b", to: "foo#bar"
|
|
|
|
path, _ = @formatter.generate(nil, { :controller => "foo", :action => "bar", :a => 'a' }, { :b => 'b' })
|
|
assert_equal "/messages/a/b", path
|
|
end
|
|
|
|
def test_splat_in_recall
|
|
get "/*path", to: "foo#bar"
|
|
|
|
path, _ = @formatter.generate(nil, { :controller => "foo", :action => "bar" }, { :path => 'b' })
|
|
assert_equal "/b", path
|
|
end
|
|
|
|
def test_recall_should_be_used_when_scoring
|
|
get "/messages/:action(/:id(.:format))", to: 'foo#bar'
|
|
get "/messages/:id(.:format)", to: 'bar#baz'
|
|
|
|
path, _ = @formatter.generate(nil, { :controller => "foo", :id => 10 }, { :action => 'index' })
|
|
assert_equal "/messages/index/10", path
|
|
end
|
|
|
|
def test_nil_path_parts_are_ignored
|
|
get "/:controller(/:action(.:format))", to: "tasks#lol"
|
|
|
|
params = { :controller => "tasks", :format => nil }
|
|
extras = { :action => 'lol' }
|
|
|
|
path, _ = @formatter.generate(nil, params, extras)
|
|
assert_equal '/tasks', path
|
|
end
|
|
|
|
def test_generate_slash
|
|
params = [ [:controller, "tasks"],
|
|
[:action, "show"] ]
|
|
get "/", Hash[params]
|
|
|
|
path, _ = @formatter.generate(nil, Hash[params], {})
|
|
assert_equal '/', path
|
|
end
|
|
|
|
def test_generate_calls_param_proc
|
|
get '/:controller(/:action)', to: "foo#bar"
|
|
|
|
parameterized = []
|
|
params = [ [:controller, "tasks"],
|
|
[:action, "show"] ]
|
|
|
|
@formatter.generate(
|
|
nil,
|
|
Hash[params],
|
|
{},
|
|
lambda { |k,v| parameterized << [k,v]; v })
|
|
|
|
assert_equal params.map(&:to_s).sort, parameterized.map(&:to_s).sort
|
|
end
|
|
|
|
def test_generate_id
|
|
get '/:controller(/:action)', to: 'foo#bar'
|
|
|
|
path, params = @formatter.generate(
|
|
nil, {:id=>1, :controller=>"tasks", :action=>"show"}, {})
|
|
assert_equal '/tasks/show', path
|
|
assert_equal({:id => 1}, params)
|
|
end
|
|
|
|
def test_generate_escapes
|
|
get '/:controller(/:action)', to: "foo#bar"
|
|
|
|
path, _ = @formatter.generate(nil,
|
|
{ :controller => "tasks",
|
|
:action => "a/b c+d",
|
|
}, {})
|
|
assert_equal '/tasks/a%2Fb%20c+d', path
|
|
end
|
|
|
|
def test_generate_escapes_with_namespaced_controller
|
|
get '/:controller(/:action)', to: "foo#bar"
|
|
|
|
path, _ = @formatter.generate(
|
|
nil, { :controller => "admin/tasks",
|
|
:action => "a/b c+d",
|
|
}, {})
|
|
assert_equal '/admin/tasks/a%2Fb%20c+d', path
|
|
end
|
|
|
|
def test_generate_extra_params
|
|
get '/:controller(/:action)', to: "foo#bar"
|
|
|
|
path, params = @formatter.generate(
|
|
nil, { :id => 1,
|
|
:controller => "tasks",
|
|
:action => "show",
|
|
:relative_url_root => nil
|
|
}, {})
|
|
assert_equal '/tasks/show', path
|
|
assert_equal({:id => 1, :relative_url_root => nil}, params)
|
|
end
|
|
|
|
def test_generate_missing_keys_no_matches_different_format_keys
|
|
get '/:controller/:action/:name', to: "foo#bar"
|
|
primarty_parameters = {
|
|
:id => 1,
|
|
:controller => "tasks",
|
|
:action => "show",
|
|
:relative_url_root => nil
|
|
}
|
|
redirection_parameters = {
|
|
'action'=>'show',
|
|
}
|
|
missing_key = 'name'
|
|
missing_parameters ={
|
|
missing_key => "task_1"
|
|
}
|
|
request_parameters = primarty_parameters.merge(redirection_parameters).merge(missing_parameters)
|
|
|
|
message = "No route matches #{Hash[request_parameters.sort_by{|k,v|k.to_s}].inspect} missing required keys: #{[missing_key.to_sym].inspect}"
|
|
|
|
error = assert_raises(ActionController::UrlGenerationError) do
|
|
@formatter.generate(
|
|
nil, request_parameters, request_parameters)
|
|
end
|
|
assert_equal message, error.message
|
|
end
|
|
|
|
def test_generate_uses_recall_if_needed
|
|
get '/:controller(/:action(/:id))', to: "foo#bar"
|
|
|
|
path, params = @formatter.generate(
|
|
nil,
|
|
{:controller =>"tasks", :id => 10},
|
|
{:action =>"index"})
|
|
assert_equal '/tasks/index/10', path
|
|
assert_equal({}, params)
|
|
end
|
|
|
|
def test_generate_with_name
|
|
get '/:controller(/:action)', to: 'foo#bar', as: 'tasks'
|
|
|
|
path, params = @formatter.generate(
|
|
"tasks",
|
|
{:controller=>"tasks"},
|
|
{:controller=>"tasks", :action=>"index"})
|
|
assert_equal '/tasks', path
|
|
assert_equal({}, params)
|
|
end
|
|
|
|
{
|
|
'/content' => { :controller => 'content' },
|
|
'/content/list' => { :controller => 'content', :action => 'list' },
|
|
'/content/show/10' => { :controller => 'content', :action => 'show', :id => "10" },
|
|
}.each do |request_path, expected|
|
|
define_method("test_recognize_#{expected.keys.map(&:to_s).join('_')}") do
|
|
get "/:controller(/:action(/:id))", to: 'foo#bar'
|
|
route = @routes.first
|
|
|
|
env = rails_env 'PATH_INFO' => request_path
|
|
called = false
|
|
|
|
router.recognize(env) do |r, params|
|
|
assert_equal route, r
|
|
assert_equal({ :action => "bar" }.merge(expected), params)
|
|
called = true
|
|
end
|
|
|
|
assert called
|
|
end
|
|
end
|
|
|
|
{
|
|
:segment => ['/a%2Fb%20c+d/splat', { :segment => 'a/b c+d', :splat => 'splat' }],
|
|
:splat => ['/segment/a/b%20c+d', { :segment => 'segment', :splat => 'a/b c+d' }]
|
|
}.each do |name, (request_path, expected)|
|
|
define_method("test_recognize_#{name}") do
|
|
get '/:segment/*splat', to: 'foo#bar'
|
|
|
|
env = rails_env 'PATH_INFO' => request_path
|
|
called = false
|
|
route = @routes.first
|
|
|
|
router.recognize(env) do |r, params|
|
|
assert_equal route, r
|
|
assert_equal(expected.merge(:controller=>"foo", :action=>"bar"), params)
|
|
called = true
|
|
end
|
|
|
|
assert called
|
|
end
|
|
end
|
|
|
|
def test_namespaced_controller
|
|
get "/:controller(/:action(/:id))", { :controller => /.+?/ }
|
|
route = @routes.first
|
|
|
|
env = rails_env 'PATH_INFO' => '/admin/users/show/10'
|
|
called = false
|
|
expected = {
|
|
:controller => 'admin/users',
|
|
:action => 'show',
|
|
:id => '10'
|
|
}
|
|
|
|
router.recognize(env) do |r, params|
|
|
assert_equal route, r
|
|
assert_equal(expected, params)
|
|
called = true
|
|
end
|
|
assert called
|
|
end
|
|
|
|
def test_recognize_literal
|
|
get "/books(/:action(.:format))", controller: "books"
|
|
route = @routes.first
|
|
|
|
env = rails_env 'PATH_INFO' => '/books/list.rss'
|
|
expected = { :controller => 'books', :action => 'list', :format => 'rss' }
|
|
called = false
|
|
router.recognize(env) do |r, params|
|
|
assert_equal route, r
|
|
assert_equal(expected, params)
|
|
called = true
|
|
end
|
|
|
|
assert called
|
|
end
|
|
|
|
def test_recognize_head_route
|
|
match "/books(/:action(.:format))", via: 'head', to: 'foo#bar'
|
|
|
|
env = rails_env(
|
|
'PATH_INFO' => '/books/list.rss',
|
|
'REQUEST_METHOD' => 'HEAD'
|
|
)
|
|
|
|
called = false
|
|
router.recognize(env) do |r, params|
|
|
called = true
|
|
end
|
|
|
|
assert called
|
|
end
|
|
|
|
def test_recognize_head_request_as_get_route
|
|
get "/books(/:action(.:format))", to: 'foo#bar'
|
|
|
|
env = rails_env 'PATH_INFO' => '/books/list.rss',
|
|
"REQUEST_METHOD" => "HEAD"
|
|
|
|
called = false
|
|
router.recognize(env) do |r, params|
|
|
called = true
|
|
end
|
|
|
|
assert called
|
|
end
|
|
|
|
def test_recognize_cares_about_get_verbs
|
|
match "/books(/:action(.:format))", to: "foo#bar", via: :get
|
|
|
|
env = rails_env 'PATH_INFO' => '/books/list.rss',
|
|
"REQUEST_METHOD" => "POST"
|
|
|
|
called = false
|
|
router.recognize(env) do |r, params|
|
|
called = true
|
|
end
|
|
|
|
assert_not called
|
|
end
|
|
|
|
def test_recognize_cares_about_post_verbs
|
|
match "/books(/:action(.:format))", to: "foo#bar", via: :post
|
|
|
|
env = rails_env 'PATH_INFO' => '/books/list.rss',
|
|
"REQUEST_METHOD" => "POST"
|
|
|
|
called = false
|
|
router.recognize(env) do |r, params|
|
|
called = true
|
|
end
|
|
|
|
assert called
|
|
end
|
|
|
|
def test_multi_verb_recognition
|
|
match "/books(/:action(.:format))", to: "foo#bar", via: [:post, :get]
|
|
|
|
%w( POST GET ).each do |verb|
|
|
env = rails_env 'PATH_INFO' => '/books/list.rss',
|
|
"REQUEST_METHOD" => verb
|
|
|
|
called = false
|
|
router.recognize(env) do |r, params|
|
|
called = true
|
|
end
|
|
|
|
assert called
|
|
end
|
|
|
|
env = rails_env 'PATH_INFO' => '/books/list.rss',
|
|
"REQUEST_METHOD" => 'PUT'
|
|
|
|
called = false
|
|
router.recognize(env) do |r, params|
|
|
called = true
|
|
end
|
|
|
|
assert_not called
|
|
end
|
|
|
|
private
|
|
|
|
def get *args
|
|
ActiveSupport::Deprecation.silence do
|
|
mapper.get(*args)
|
|
end
|
|
end
|
|
|
|
def match *args
|
|
ActiveSupport::Deprecation.silence do
|
|
mapper.match(*args)
|
|
end
|
|
end
|
|
|
|
def rails_env env, klass = ActionDispatch::Request
|
|
klass.new(rack_env(env))
|
|
end
|
|
|
|
def rack_env env
|
|
{
|
|
"rack.version" => [1, 1],
|
|
"rack.input" => StringIO.new,
|
|
"rack.errors" => StringIO.new,
|
|
"rack.multithread" => true,
|
|
"rack.multiprocess" => true,
|
|
"rack.run_once" => false,
|
|
"REQUEST_METHOD" => "GET",
|
|
"SERVER_NAME" => "example.org",
|
|
"SERVER_PORT" => "80",
|
|
"QUERY_STRING" => "",
|
|
"PATH_INFO" => "/content",
|
|
"rack.url_scheme" => "http",
|
|
"HTTPS" => "off",
|
|
"SCRIPT_NAME" => "",
|
|
"CONTENT_LENGTH" => "0"
|
|
}.merge env
|
|
end
|
|
end
|
|
end
|
|
end
|