Spree API has Unauthenticated IDOR - Guest Address
Description
Spree is an open source e-commerce solution built with Ruby on Rails. Prior to versions 4.10.2, 5.0.7, 5.1.9, and 5.2.5, an Unauthenticated Insecure Direct Object Reference (IDOR) vulnerability was identified that allows an unauthenticated attacker to access guest address information without supplying valid credentials or session cookies. This issue has been patched in versions 4.10.2, 5.0.7, 5.1.9, and 5.2.5.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
spree_coreRubyGems | >= 4.0.0, < 4.10.2 | 4.10.2 |
spree_coreRubyGems | >= 5.0.0, < 5.0.7 | 5.0.7 |
spree_coreRubyGems | >= 5.1.0, < 5.1.9 | 5.1.9 |
spree_coreRubyGems | >= 5.2.0, < 5.2.5 | 5.2.5 |
Affected products
1Patches
4d051925778f2Fixes GHSA-3ghg-3787-w2xr Unauthenticated IDOR - Guest Address (#13422)
5 files changed · +213 −2
core/app/models/spree/ability.rb+2 −1 modified@@ -78,7 +78,8 @@ def apply_user_permissions(user, _options) can :update, ::Spree::Order do |order, token| !order.completed? && (order.user == user || order.token && token == order.token) end - can :manage, ::Spree::Address, user_id: user.id + # Address management - only for persisted users with matching user_id + can :manage, ::Spree::Address, user_id: user.id if user.persisted? can [:read, :destroy], ::Spree::CreditCard, user_id: user.id can :read, ::Spree::Product can :read, ::Spree::ProductProperty
core/spec/models/spree/ability_spec.rb+49 −0 modified@@ -293,5 +293,54 @@ def initialize(_user) it_behaves_like 'read only' end end + + context 'for Address (IDOR vulnerability prevention)' do + let(:guest_address) { create(:address, user_id: nil) } + + context 'with non-persisted guest user' do + let(:guest_user) { Spree.user_class.new } + let(:guest_ability) { Spree::Ability.new(guest_user) } + + it 'cannot read guest addresses with nil user_id' do + expect(guest_ability).not_to be_able_to :read, guest_address + end + + it 'cannot edit guest addresses with nil user_id' do + expect(guest_ability).not_to be_able_to :edit, guest_address + end + + it 'cannot update guest addresses with nil user_id' do + expect(guest_ability).not_to be_able_to :update, guest_address + end + + it 'cannot destroy guest addresses with nil user_id' do + expect(guest_ability).not_to be_able_to :destroy, guest_address + end + + it 'cannot manage any address' do + expect(guest_ability).not_to be_able_to :manage, guest_address + end + end + + context 'with persisted user' do + let(:persisted_user) { create(:user) } + let(:persisted_ability) { Spree::Ability.new(persisted_user) } + let(:own_address) { create(:address, user_id: persisted_user.id) } + + it 'can manage own address' do + expect(persisted_ability).to be_able_to :manage, own_address + end + + it 'cannot manage guest addresses' do + expect(persisted_ability).not_to be_able_to :manage, guest_address + end + + it 'cannot manage other user addresses' do + other_user = create(:user) + other_address = create(:address, user_id: other_user.id) + expect(persisted_ability).not_to be_able_to :manage, other_address + end + end + end end end
storefront/app/controllers/spree/addresses_controller.rb+1 −0 modified@@ -1,6 +1,7 @@ module Spree class AddressesController < Spree::StoreController helper Spree::AddressesHelper + before_action :require_user before_action :load_and_authorize_address, except: [:index] def create
storefront/app/controllers/spree/store_controller.rb+5 −1 modified@@ -193,7 +193,11 @@ def storefront_products_includes def redirect_unauthorized_access if try_spree_current_user flash[:error] = Spree.t(:authorization_failure) - redirect_to spree.forbidden_path + if spree.respond_to?(:forbidden_path) + redirect_to spree.forbidden_path + else + redirect_to spree.root_path + end else store_location if respond_to?(:spree_login_path)
storefront/spec/controllers/spree/addresses_controller_spec.rb+156 −0 modified@@ -312,4 +312,160 @@ end end end + +end + +# Separate describe block for security tests - without mocked authorization +describe Spree::AddressesController, 'security', type: :controller do + let(:store) { @default_store } + let(:country) { store.default_country || create(:country_us) } + let(:state) { create(:state, country: country, name: 'New York', abbr: 'NY') } + let(:user) { create(:user) } + + render_views + + context 'authentication requirement (IDOR vulnerability fix)' do + let(:guest_address) { create(:address, user_id: nil, country: country, state: state) } + + before do + allow(controller).to receive_messages try_spree_current_user: nil + allow(controller).to receive_messages spree_current_user: nil + end + + describe '#edit' do + it 'requires authentication and redirects to login' do + get :edit, params: { id: guest_address.id } + expect(response).to have_http_status(:redirect) + end + end + + describe '#update' do + let(:address_params) { guest_address.attributes.symbolize_keys.merge(firstname: 'Hacker') } + + it 'requires authentication and redirects to login' do + put :update, params: { id: guest_address.id, address: address_params } + expect(response).to have_http_status(:redirect) + end + + it 'does not update the address when not authenticated' do + original_firstname = guest_address.firstname + put :update, params: { id: guest_address.id, address: address_params } + expect(guest_address.reload.firstname).to eq(original_firstname) + end + end + + describe '#destroy' do + it 'requires authentication and redirects to login' do + delete :destroy, params: { id: guest_address.id } + expect(response).to have_http_status(:redirect) + end + + it 'does not destroy the address when not authenticated' do + delete :destroy, params: { id: guest_address.id } + expect(Spree::Address.find_by(id: guest_address.id)).to be_present + end + end + + describe '#new' do + it 'requires authentication and redirects to login' do + get :new + expect(response).to have_http_status(:redirect) + end + end + + describe '#create' do + let(:address_params) do + build(:address, country: country, state: state).attributes.except('created_at', 'updated_at') + end + + it 'requires authentication and redirects to login' do + post :create, params: { address: address_params } + expect(response).to have_http_status(:redirect) + end + end + end + + context 'when authenticated user tries to access another users address' do + let(:other_user) { create(:user) } + let(:other_user_address) { create(:address, user: other_user, country: country, state: state) } + + before do + allow(controller).to receive_messages try_spree_current_user: user + allow(controller).to receive_messages spree_current_user: user + end + + describe '#edit' do + it 'denies access to other users address' do + get :edit, params: { id: other_user_address.id } + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(spree.root_path) + expect(flash[:error]).to eq(Spree.t(:authorization_failure)) + end + end + + describe '#update' do + let(:address_params) { other_user_address.attributes.symbolize_keys.merge(firstname: 'Hacker') } + + it 'denies update to other users address' do + put :update, params: { id: other_user_address.id, address: address_params } + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(spree.root_path) + expect(flash[:error]).to eq(Spree.t(:authorization_failure)) + end + + it 'does not update the address when denied' do + original_firstname = other_user_address.firstname + put :update, params: { id: other_user_address.id, address: address_params } + expect(other_user_address.reload.firstname).to eq(original_firstname) + end + end + + describe '#destroy' do + it 'denies destroy of other users address' do + delete :destroy, params: { id: other_user_address.id } + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(spree.root_path) + expect(flash[:error]).to eq(Spree.t(:authorization_failure)) + end + + it 'does not destroy the address when denied' do + delete :destroy, params: { id: other_user_address.id } + expect(Spree::Address.find_by(id: other_user_address.id)).to be_present + end + end + end + + context 'when authenticated user accesses their own address' do + let(:own_address) { create(:address, user: user, country: country, state: state) } + + before do + allow(controller).to receive_messages try_spree_current_user: user + allow(controller).to receive_messages spree_current_user: user + end + + describe '#edit' do + it 'allows access to own address' do + get :edit, params: { id: own_address.id } + expect(response).to have_http_status(:ok) + end + end + + describe '#update' do + let(:address_params) { own_address.attributes.symbolize_keys.merge(firstname: 'Updated') } + + it 'allows update to own address' do + put :update, params: { id: own_address.id, address: address_params } + expect(response).to have_http_status(:redirect) + expect(response).not_to redirect_to(spree.root_path) + expect(flash[:error]).to be_nil + end + end + + describe '#destroy' do + it 'allows destroy of own address' do + delete :destroy, params: { id: own_address.id } + expect(response).to have_http_status(:see_other) + end + end + end end
e1cff4605eb1Fixes GHSA-3ghg-3787-w2xr Unauthenticated IDOR - Guest Address (#13422)
6 files changed · +232 −3
core/app/models/spree/ability.rb+2 −1 modified@@ -143,7 +143,8 @@ def apply_user_permissions(user, _options) can :update, ::Spree::Order do |order, token| !order.completed? && (order.user == user || order.token && token == order.token) end - can :manage, ::Spree::Address, user_id: user.id + # Address management - only for persisted users with matching user_id + can :manage, ::Spree::Address, user_id: user.id if user.persisted? can [:read, :destroy], ::Spree::CreditCard, user_id: user.id can :read, ::Spree::Product can :read, ::Spree::ProductProperty
core/app/models/spree/permission_sets/default_customer.rb+2 −2 modified@@ -43,8 +43,8 @@ def activate! can :create, Spree.user_class can [:show, :update, :destroy], Spree.user_class, id: user.id - # Address management - can :manage, Spree::Address, user_id: user.id + # Address management - only for persisted users with matching user_id + can :manage, Spree::Address, user_id: user.id if user.persisted? # Credit card management can [:read, :destroy], Spree::CreditCard, user_id: user.id
core/spec/models/spree/ability_spec.rb+49 −0 modified@@ -293,5 +293,54 @@ def initialize(_user) it_behaves_like 'read only' end end + + context 'for Address (IDOR vulnerability prevention)' do + let(:guest_address) { create(:address, user_id: nil) } + + context 'with non-persisted guest user' do + let(:guest_user) { Spree.user_class.new } + let(:guest_ability) { Spree::Ability.new(guest_user) } + + it 'cannot read guest addresses with nil user_id' do + expect(guest_ability).not_to be_able_to :read, guest_address + end + + it 'cannot edit guest addresses with nil user_id' do + expect(guest_ability).not_to be_able_to :edit, guest_address + end + + it 'cannot update guest addresses with nil user_id' do + expect(guest_ability).not_to be_able_to :update, guest_address + end + + it 'cannot destroy guest addresses with nil user_id' do + expect(guest_ability).not_to be_able_to :destroy, guest_address + end + + it 'cannot manage any address' do + expect(guest_ability).not_to be_able_to :manage, guest_address + end + end + + context 'with persisted user' do + let(:persisted_user) { create(:user) } + let(:persisted_ability) { Spree::Ability.new(persisted_user) } + let(:own_address) { create(:address, user_id: persisted_user.id) } + + it 'can manage own address' do + expect(persisted_ability).to be_able_to :manage, own_address + end + + it 'cannot manage guest addresses' do + expect(persisted_ability).not_to be_able_to :manage, guest_address + end + + it 'cannot manage other user addresses' do + other_user = create(:user) + other_address = create(:address, user_id: other_user.id) + expect(persisted_ability).not_to be_able_to :manage, other_address + end + end + end end end
core/spec/models/spree/permission_sets/default_customer_spec.rb+26 −0 modified@@ -123,6 +123,32 @@ it 'prevents managing other user address' do expect(ability.can?(:manage, other_address)).to be false end + + context 'with guest user (non-persisted)' do + let(:guest_user) { Spree.user_class.new } + let(:guest_ability) { Spree::Ability.new(guest_user) } + let(:guest_permission_set) { described_class.new(guest_ability) } + let(:guest_address) { build(:address, user_id: nil) } + let(:other_guest_address) { create(:address, user_id: nil) } + + before { guest_permission_set.activate! } + + it 'prevents guest user from managing addresses with nil user_id (IDOR protection)' do + expect(guest_ability.can?(:manage, guest_address)).to be false + end + + it 'prevents guest user from editing other guest addresses (IDOR protection)' do + expect(guest_ability.can?(:edit, other_guest_address)).to be false + end + + it 'prevents guest user from updating other guest addresses (IDOR protection)' do + expect(guest_ability.can?(:update, other_guest_address)).to be false + end + + it 'prevents guest user from reading other guest addresses (IDOR protection)' do + expect(guest_ability.can?(:read, other_guest_address)).to be false + end + end end context 'credit card permissions' do
storefront/app/controllers/spree/addresses_controller.rb+1 −0 modified@@ -1,6 +1,7 @@ module Spree class AddressesController < Spree::StoreController helper Spree::AddressesHelper + before_action :require_user before_action :load_and_authorize_address, except: [:index] def create
storefront/spec/controllers/spree/addresses_controller_spec.rb+152 −0 modified@@ -312,4 +312,156 @@ end end end + +end + +# Separate describe block for security tests - without mocked authorization +describe Spree::AddressesController, 'security', type: :controller do + let(:store) { @default_store } + let(:country) { store.default_country || create(:country_us) } + let(:state) { create(:state, country: country, name: 'New York', abbr: 'NY') } + let(:user) { create(:user) } + + render_views + + context 'authentication requirement (IDOR vulnerability fix)' do + let(:guest_address) { create(:address, user_id: nil, country: country, state: state) } + + before do + allow(controller).to receive_messages try_spree_current_user: nil + allow(controller).to receive_messages spree_current_user: nil + end + + describe '#edit' do + it 'requires authentication and redirects to login' do + get :edit, params: { id: guest_address.id } + expect(response).to have_http_status(:redirect) + end + end + + describe '#update' do + let(:address_params) { guest_address.attributes.symbolize_keys.merge(firstname: 'Hacker') } + + it 'requires authentication and redirects to login' do + put :update, params: { id: guest_address.id, address: address_params } + expect(response).to have_http_status(:redirect) + end + + it 'does not update the address when not authenticated' do + original_firstname = guest_address.firstname + put :update, params: { id: guest_address.id, address: address_params } + expect(guest_address.reload.firstname).to eq(original_firstname) + end + end + + describe '#destroy' do + it 'requires authentication and redirects to login' do + delete :destroy, params: { id: guest_address.id } + expect(response).to have_http_status(:redirect) + end + + it 'does not destroy the address when not authenticated' do + delete :destroy, params: { id: guest_address.id } + expect(Spree::Address.find_by(id: guest_address.id)).to be_present + end + end + + describe '#new' do + it 'requires authentication and redirects to login' do + get :new + expect(response).to have_http_status(:redirect) + end + end + + describe '#create' do + let(:address_params) do + build(:address, country: country, state: state).attributes.except('created_at', 'updated_at') + end + + it 'requires authentication and redirects to login' do + post :create, params: { address: address_params } + expect(response).to have_http_status(:redirect) + end + end + end + + context 'when authenticated user tries to access another users address' do + let(:other_user) { create(:user) } + let(:other_user_address) { create(:address, user: other_user, country: country, state: state) } + + before do + allow(controller).to receive_messages try_spree_current_user: user + allow(controller).to receive_messages spree_current_user: user + end + + describe '#edit' do + it 'denies access to other users address' do + get :edit, params: { id: other_user_address.id } + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(spree.forbidden_path) + end + end + + describe '#update' do + let(:address_params) { other_user_address.attributes.symbolize_keys.merge(firstname: 'Hacker') } + + it 'denies update to other users address' do + put :update, params: { id: other_user_address.id, address: address_params } + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(spree.forbidden_path) + end + + it 'does not update the address when denied' do + original_firstname = other_user_address.firstname + put :update, params: { id: other_user_address.id, address: address_params } + expect(other_user_address.reload.firstname).to eq(original_firstname) + end + end + + describe '#destroy' do + it 'denies destroy of other users address' do + delete :destroy, params: { id: other_user_address.id } + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(spree.forbidden_path) + end + + it 'does not destroy the address when denied' do + delete :destroy, params: { id: other_user_address.id } + expect(Spree::Address.find_by(id: other_user_address.id)).to be_present + end + end + end + + context 'when authenticated user accesses their own address' do + let(:own_address) { create(:address, user: user, country: country, state: state) } + + before do + allow(controller).to receive_messages try_spree_current_user: user + allow(controller).to receive_messages spree_current_user: user + end + + describe '#edit' do + it 'allows access to own address' do + get :edit, params: { id: own_address.id } + expect(response).to have_http_status(:ok) + end + end + + describe '#update' do + let(:address_params) { own_address.attributes.symbolize_keys.merge(firstname: 'Updated') } + + it 'allows update to own address' do + put :update, params: { id: own_address.id, address: address_params } + expect(response).to have_http_status(:redirect) + expect(response).not_to redirect_to(spree.forbidden_path) + end + end + + describe '#destroy' do + it 'allows destroy of own address' do + delete :destroy, params: { id: own_address.id } + expect(response).to have_http_status(:see_other) + end + end + end end
16067def6de8Fixes GHSA-3ghg-3787-w2xr Unauthenticated IDOR - Guest Address (#13422)
5 files changed · +214 −2
core/app/models/spree/ability.rb+2 −1 modified@@ -75,7 +75,8 @@ def apply_user_permissions(user) can :update, ::Spree::Order do |order, token| !order.completed? && (order.user == user || order.token && token == order.token) end - can :manage, ::Spree::Address, user_id: user.id + # Address management - only for persisted users with matching user_id + can :manage, ::Spree::Address, user_id: user.id if user.persisted? can [:read, :destroy], ::Spree::CreditCard, user_id: user.id can :read, ::Spree::Product can :read, ::Spree::ProductProperty
core/spec/models/spree/ability_spec.rb+49 −0 modified@@ -293,5 +293,54 @@ def initialize(_user) it_behaves_like 'read only' end end + + context 'for Address (IDOR vulnerability prevention)' do + let(:guest_address) { create(:address, user_id: nil) } + + context 'with non-persisted guest user' do + let(:guest_user) { Spree.user_class.new } + let(:guest_ability) { Spree::Ability.new(guest_user) } + + it 'cannot read guest addresses with nil user_id' do + expect(guest_ability).not_to be_able_to :read, guest_address + end + + it 'cannot edit guest addresses with nil user_id' do + expect(guest_ability).not_to be_able_to :edit, guest_address + end + + it 'cannot update guest addresses with nil user_id' do + expect(guest_ability).not_to be_able_to :update, guest_address + end + + it 'cannot destroy guest addresses with nil user_id' do + expect(guest_ability).not_to be_able_to :destroy, guest_address + end + + it 'cannot manage any address' do + expect(guest_ability).not_to be_able_to :manage, guest_address + end + end + + context 'with persisted user' do + let(:persisted_user) { create(:user) } + let(:persisted_ability) { Spree::Ability.new(persisted_user) } + let(:own_address) { create(:address, user_id: persisted_user.id) } + + it 'can manage own address' do + expect(persisted_ability).to be_able_to :manage, own_address + end + + it 'cannot manage guest addresses' do + expect(persisted_ability).not_to be_able_to :manage, guest_address + end + + it 'cannot manage other user addresses' do + other_user = create(:user) + other_address = create(:address, user_id: other_user.id) + expect(persisted_ability).not_to be_able_to :manage, other_address + end + end + end end end
storefront/app/controllers/spree/addresses_controller.rb+2 −0 modified@@ -1,6 +1,8 @@ module Spree class AddressesController < Spree::StoreController helper Spree::AddressesHelper + + before_action :require_user load_and_authorize_resource class: Spree::Address def create
storefront/app/controllers/spree/store_controller.rb+5 −1 modified@@ -181,7 +181,11 @@ def storefront_products_includes def redirect_unauthorized_access if try_spree_current_user flash[:error] = Spree.t(:authorization_failure) - redirect_to spree.forbidden_path + if spree.respond_to?(:forbidden_path) + redirect_to spree.forbidden_path + else + redirect_to spree.root_path + end else store_location if respond_to?(:spree_login_path)
storefront/spec/controllers/spree/addresses_controller_spec.rb+156 −0 modified@@ -209,4 +209,160 @@ end end end + +end + +# Separate describe block for security tests - without mocked authorization +describe Spree::AddressesController, 'security', type: :controller do + let(:store) { @default_store } + let(:country) { store.default_country || create(:country_us) } + let(:state) { create(:state, country: country, name: 'New York', abbr: 'NY') } + let(:user) { create(:user) } + + render_views + + context 'authentication requirement (IDOR vulnerability fix)' do + let(:guest_address) { create(:address, user_id: nil, country: country, state: state) } + + before do + allow(controller).to receive_messages try_spree_current_user: nil + allow(controller).to receive_messages spree_current_user: nil + end + + describe '#edit' do + it 'requires authentication and redirects to login' do + get :edit, params: { id: guest_address.id } + expect(response).to have_http_status(:redirect) + end + end + + describe '#update' do + let(:address_params) { guest_address.attributes.symbolize_keys.merge(firstname: 'Hacker') } + + it 'requires authentication and redirects to login' do + put :update, params: { id: guest_address.id, address: address_params } + expect(response).to have_http_status(:redirect) + end + + it 'does not update the address when not authenticated' do + original_firstname = guest_address.firstname + put :update, params: { id: guest_address.id, address: address_params } + expect(guest_address.reload.firstname).to eq(original_firstname) + end + end + + describe '#destroy' do + it 'requires authentication and redirects to login' do + delete :destroy, params: { id: guest_address.id } + expect(response).to have_http_status(:redirect) + end + + it 'does not destroy the address when not authenticated' do + delete :destroy, params: { id: guest_address.id } + expect(Spree::Address.find_by(id: guest_address.id)).to be_present + end + end + + describe '#new' do + it 'requires authentication and redirects to login' do + get :new + expect(response).to have_http_status(:redirect) + end + end + + describe '#create' do + let(:address_params) do + build(:address, country: country, state: state).attributes.except('created_at', 'updated_at') + end + + it 'requires authentication and redirects to login' do + post :create, params: { address: address_params } + expect(response).to have_http_status(:redirect) + end + end + end + + context 'when authenticated user tries to access another users address' do + let(:other_user) { create(:user) } + let(:other_user_address) { create(:address, user: other_user, country: country, state: state) } + + before do + allow(controller).to receive_messages try_spree_current_user: user + allow(controller).to receive_messages spree_current_user: user + end + + describe '#edit' do + it 'denies access to other users address' do + get :edit, params: { id: other_user_address.id } + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(spree.root_path) + expect(flash[:error]).to eq(Spree.t(:authorization_failure)) + end + end + + describe '#update' do + let(:address_params) { other_user_address.attributes.symbolize_keys.merge(firstname: 'Hacker') } + + it 'denies update to other users address' do + put :update, params: { id: other_user_address.id, address: address_params } + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(spree.root_path) + expect(flash[:error]).to eq(Spree.t(:authorization_failure)) + end + + it 'does not update the address when denied' do + original_firstname = other_user_address.firstname + put :update, params: { id: other_user_address.id, address: address_params } + expect(other_user_address.reload.firstname).to eq(original_firstname) + end + end + + describe '#destroy' do + it 'denies destroy of other users address' do + delete :destroy, params: { id: other_user_address.id } + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(spree.root_path) + expect(flash[:error]).to eq(Spree.t(:authorization_failure)) + end + + it 'does not destroy the address when denied' do + delete :destroy, params: { id: other_user_address.id } + expect(Spree::Address.find_by(id: other_user_address.id)).to be_present + end + end + end + + context 'when authenticated user accesses their own address' do + let(:own_address) { create(:address, user: user, country: country, state: state) } + + before do + allow(controller).to receive_messages try_spree_current_user: user + allow(controller).to receive_messages spree_current_user: user + end + + describe '#edit' do + it 'allows access to own address' do + get :edit, params: { id: own_address.id } + expect(response).to have_http_status(:ok) + end + end + + describe '#update' do + let(:address_params) { own_address.attributes.symbolize_keys.merge(firstname: 'Updated') } + + it 'allows update to own address' do + put :update, params: { id: own_address.id, address: address_params } + expect(response).to have_http_status(:redirect) + expect(response).not_to redirect_to(spree.root_path) + expect(flash[:error]).to be_nil + end + end + + describe '#destroy' do + it 'allows destroy of own address' do + delete :destroy, params: { id: own_address.id } + expect(response).to have_http_status(:see_other) + end + end + end end
4c2bd62326fbFixes GHSA-3ghg-3787-w2xr Unauthenticated IDOR - Guest Address (#13422)
2 files changed · +51 −1
core/app/models/spree/ability.rb+2 −1 modified@@ -73,7 +73,8 @@ def apply_user_permissions(user) can :update, ::Spree::Order do |order, token| !order.completed? && (order.user == user || order.token && token == order.token) end - can :manage, ::Spree::Address, user_id: user.id + # Address management - only for persisted users with matching user_id + can :manage, ::Spree::Address, user_id: user.id if user.persisted? can [:read, :destroy], ::Spree::CreditCard, user_id: user.id can :read, ::Spree::Product can :read, ::Spree::ProductProperty
core/spec/models/spree/ability_spec.rb+49 −0 modified@@ -293,5 +293,54 @@ def initialize(_user) it_behaves_like 'read only' end end + + context 'for Address (IDOR vulnerability prevention)' do + let(:guest_address) { create(:address, user_id: nil) } + + context 'with non-persisted guest user' do + let(:guest_user) { Spree.user_class.new } + let(:guest_ability) { Spree::Ability.new(guest_user) } + + it 'cannot read guest addresses with nil user_id' do + expect(guest_ability).not_to be_able_to :read, guest_address + end + + it 'cannot edit guest addresses with nil user_id' do + expect(guest_ability).not_to be_able_to :edit, guest_address + end + + it 'cannot update guest addresses with nil user_id' do + expect(guest_ability).not_to be_able_to :update, guest_address + end + + it 'cannot destroy guest addresses with nil user_id' do + expect(guest_ability).not_to be_able_to :destroy, guest_address + end + + it 'cannot manage any address' do + expect(guest_ability).not_to be_able_to :manage, guest_address + end + end + + context 'with persisted user' do + let(:persisted_user) { create(:user) } + let(:persisted_ability) { Spree::Ability.new(persisted_user) } + let(:own_address) { create(:address, user_id: persisted_user.id) } + + it 'can manage own address' do + expect(persisted_ability).to be_able_to :manage, own_address + end + + it 'cannot manage guest addresses' do + expect(persisted_ability).not_to be_able_to :manage, guest_address + end + + it 'cannot manage other user addresses' do + other_user = create(:user) + other_address = create(:address, user_id: other_user.id) + expect(persisted_ability).not_to be_able_to :manage, other_address + end + end + end end end
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
8- github.com/advisories/GHSA-3ghg-3787-w2xrghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-22589ghsaADVISORY
- github.com/rubysec/ruby-advisory-db/blob/master/gems/spree_core/CVE-2026-22589.ymlghsaWEB
- github.com/spree/spree/commit/16067def6de8e0742d55313e83b0fbab6d2fd795ghsax_refsource_MISCWEB
- github.com/spree/spree/commit/4c2bd62326fba0d846fd9e4bad2c62433829b3adghsax_refsource_MISCWEB
- github.com/spree/spree/commit/d051925778f24436b62fa8e4a6b842c72ca80a67ghsax_refsource_MISCWEB
- github.com/spree/spree/commit/e1cff4605eb15472904602aebaf8f2d04852d6adghsax_refsource_MISCWEB
- github.com/spree/spree/security/advisories/GHSA-3ghg-3787-w2xrghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.