Skip to content

Commit ec4fbe6

Browse files
Fix account takeover through CSRF attack
This commit fixes an account takeover vulnerability when [Rails `protect_from_forgery`](https://api.rubyonrails.org/classes/ActionController/RequestForgeryProtection/ClassMethods.html) method is both: - Executed whether as: - A `before_action` callback (the default) - A `prepend_before_action` (option `prepend: true`) before the `:load_object` hook in `Spree::UsersController` (most likely order to find). - Configured to use `:null_session` or `:reset_session` strategies (`:null_session` is the default in case the no strategy is given, but `rails --new` generated skeleton use `:exception`). Before this commit, the user was fetched in a `prepend_before_action` hook named `:load_object`. I.e., the user was loaded into an instance variable before touching the session as a safety countermeasure. As the request went forward, `#update` was called on that instance variable. The `:exception` strategy prevented the issue as, even if the user was still fetched, the request was aborted before the update phase. On the other hand, prepending `:protect_from_forgery` after the `:load_object` hook (not very likely, as `ApplicationController` is loaded in the first place and it's the most likely place to have that definition) wiped the session before trying to fetch the user from it. We could have fixed the most likely issue by just using a `before_action` for `:load_object`, but it's safer not to rely on the order of callbacks at all.
1 parent c03ca9a commit ec4fbe6

2 files changed

Lines changed: 48 additions & 1 deletion

File tree

lib/controllers/frontend/spree/users_controller.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22

33
class Spree::UsersController < Spree::StoreController
44
skip_before_action :set_current_order, only: :show, raise: false
5-
prepend_before_action :load_object, only: [:show, :edit, :update]
65
prepend_before_action :authorize_actions, only: :new
76

87
include Spree::Core::ControllerHelpers
98

109
def show
10+
load_object
1111
@orders = @user.orders.complete.order('completed_at desc')
1212
end
1313

@@ -25,7 +25,12 @@ def create
2525
end
2626
end
2727

28+
def edit
29+
load_object
30+
end
31+
2832
def update
33+
load_object
2934
if @user.update(user_params)
3035
spree_current_user.reload
3136
redirect_url = spree.account_url
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.feature 'User update', type: :request do
4+
context 'CSRF protection' do
5+
%i[exception reset_session null_session].each do |strategy|
6+
# Completely clean the configuration of forgery protection for the
7+
# controller and reset it after the expectations. However, besides `:with`,
8+
# the options given to `protect_from_forgery` are processed on the fly.
9+
# I.e., there's no way to retain them. The initial setup corresponds to the
10+
# dummy application, which uses the default Rails skeleton in that regard.
11+
# So, if at some point Rails changed the given options, we should update it
12+
# here.
13+
around do |example|
14+
controller = Spree::UsersController
15+
old_allow_forgery_protection_value = controller.allow_forgery_protection
16+
old_forgery_protection_strategy = controller.forgery_protection_strategy
17+
controller.skip_forgery_protection
18+
controller.allow_forgery_protection = true
19+
controller.protect_from_forgery with: strategy
20+
21+
example.run
22+
23+
controller.allow_forgery_protection = old_allow_forgery_protection_value
24+
controller.forgery_protection_strategy = old_forgery_protection_strategy
25+
end
26+
27+
it "is not possible to take account over with the #{strategy} forgery protection strategy" do
28+
user = create(:user, email: 'legit@mail.com', password: 'password')
29+
30+
post '/login', params: "spree_user[email]=legit@mail.com&spree_user[password]=password"
31+
begin
32+
put '/users/123456', params: 'user[email]=hacked@example.com'
33+
rescue
34+
# testing that the account is not compromised regardless of any raised
35+
# exception
36+
end
37+
38+
expect(user.reload.email).to eq('legit@mail.com')
39+
end
40+
end
41+
end
42+
end

0 commit comments

Comments
 (0)