Request Forgery takes relative paths into account

Passing relative paths into form_for and related helpers led to invalid
token generations, as the tokens did not match the request.path on the
POST endpoint. Variants, such as:

form_for url:
* ""
* "./"
* "./post_one"
* "post_one"

are now handled according to [RFC 3986 5.2 - 5.4](https://tools.ietf.org/html/rfc3986#section-5.2)

Limitations: double dots are not handled (../../path)

relevant issue: #31191
This commit is contained in:
Stefan Wienert 2018-04-30 23:54:11 +02:00 committed by Rafael Mendonça França
parent 05b4db2ad0
commit e2a8bfa1f2
No known key found for this signature in database
GPG Key ID: FC23B6D0F1EEE948
3 changed files with 52 additions and 0 deletions

View File

@ -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*

View File

@ -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

View File

@ -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/" }