diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index d7be6de5f58..f86b51e6393 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,3 +1,7 @@ +* Request Forgery takes relative paths into account. + + *Stefan Wienert* + * Add ".test" as a default allowed host in development to ensure smooth golden-path setup with puma.dev. *DHH* diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index 215b1bfb1b0..30492df2e99 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -635,6 +635,15 @@ module ActionController # :nodoc: def normalize_action_path(action_path) # :doc: uri = URI.parse(action_path) + + if uri.relative? && (action_path.blank? || !action_path.start_with?("/")) + uri = URI.parse(request.path) + # add the action path to the request.path + uri.path += "/#{action_path}" + # relative path with "./path" + uri.path.gsub!("/./", "/") + end + uri.path.chomp("/") end diff --git a/actionpack/test/controller/request_forgery_protection_test.rb b/actionpack/test/controller/request_forgery_protection_test.rb index 85a3e8cf5a2..de8606d49e9 100644 --- a/actionpack/test/controller/request_forgery_protection_test.rb +++ b/actionpack/test/controller/request_forgery_protection_test.rb @@ -1112,6 +1112,45 @@ class PerFormTokensControllerTest < ActionController::TestCase assert_response :success end + def test_handles_empty_path_as_request_path + get :index, params: { form_path: "" } + + form_token = assert_presence_and_fetch_form_csrf_token + + # This is required because PATH_INFO isn't reset between requests. + @request.env["PATH_INFO"] = "/per_form_tokens" + assert_nothing_raised do + post :post_one, params: { custom_authenticity_token: form_token } + end + assert_response :success + end + + def test_handles_relative_paths + get :index, params: { form_path: "post_one" } + + form_token = assert_presence_and_fetch_form_csrf_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: form_token } + end + assert_response :success + end + + def test_handles_relative_paths_with_dot + get :index, params: { form_path: "./post_one" } + + form_token = assert_presence_and_fetch_form_csrf_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: form_token } + end + assert_response :success + end + def test_ignores_origin_during_generation get :index, params: { form_path: "https://example.com/per_form_tokens/post_one/" }