VYPR
High severityOSV Advisory· Published Jan 10, 2026· Updated Jan 12, 2026

Spree API has Unauthenticated IDOR - Guest Address

CVE-2026-22589

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.

PackageAffected versionsPatched versions
spree_coreRubyGems
>= 4.0.0, < 4.10.24.10.2
spree_coreRubyGems
>= 5.0.0, < 5.0.75.0.7
spree_coreRubyGems
>= 5.1.0, < 5.1.95.1.9
spree_coreRubyGems
>= 5.2.0, < 5.2.55.2.5

Affected products

1
  • Range: v0.11.0, v0.11.99, v0.2.0, …

Patches

4
d051925778f2

Fixes GHSA-3ghg-3787-w2xr Unauthenticated IDOR - Guest Address (#13422)

https://github.com/spree/spreeDamian LegawiecJan 8, 2026via ghsa
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
    
e1cff4605eb1

Fixes GHSA-3ghg-3787-w2xr Unauthenticated IDOR - Guest Address (#13422)

https://github.com/spree/spreeDamian LegawiecJan 8, 2026via ghsa
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
    
16067def6de8

Fixes GHSA-3ghg-3787-w2xr Unauthenticated IDOR - Guest Address (#13422)

https://github.com/spree/spreeDamian LegawiecJan 8, 2026via ghsa
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
    
4c2bd62326fb

Fixes GHSA-3ghg-3787-w2xr Unauthenticated IDOR - Guest Address (#13422)

https://github.com/spree/spreeDamian LegawiecJan 8, 2026via ghsa
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

News mentions

0

No linked articles in our index yet.