Skip to content

Commit 50bf244

Browse files
[PATCH] 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 2c043f5 commit 50bf244

File tree

2 files changed

+48
-1
lines changed

2 files changed

+48
-1
lines changed

lib/controllers/frontend/spree/users_controller.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
class Spree::UsersController < Spree::StoreController
22
before_action :set_current_order, except: :show
3-
prepend_before_action :load_object, only: [:show, :edit, :update]
43
prepend_before_action :authorize_actions, only: :new
54

65
include Spree::Core::ControllerHelpers
76

87
def show
8+
load_object
99
@orders = @user.orders.complete.order('completed_at desc')
1010
end
1111

@@ -23,7 +23,12 @@ def create
2323
end
2424
end
2525

26+
def edit
27+
load_object
28+
end
29+
2630
def update
31+
load_object
2732
if @user.update(user_params)
2833
if params[:user][:password].present?
2934
# this logic needed b/c devise wants to log us out after password changes
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: '[email protected]', password: 'password')
29+
30+
post '/login', params: "spree_user[email][email protected]&spree_user[password]=password"
31+
begin
32+
put '/users/123456', params: 'user[email][email protected]'
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('[email protected]')
39+
end
40+
end
41+
end
42+
end

0 commit comments

Comments
 (0)