VYPR
Moderate severityOSV Advisory· Published Jan 8, 2026· Updated Jan 8, 2026

Spree API has Authenticated Insecure Direct Object Reference (IDOR) via Order Modification

CVE-2026-22588

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 Authenticated Insecure Direct Object Reference (IDOR) vulnerability was identified that allows an authenticated user to retrieve other users’ address information by modifying an existing order. By editing an order they legitimately own and manipulating address identifiers in the request, the backend server accepts and processes references to addresses belonging to other users, subsequently associating those addresses with the attacker’s order and returning them in the response. 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_apiRubyGems
>= 3.7.0, < 4.10.24.10.2
spree_apiRubyGems
>= 5.0.0, < 5.0.75.0.7
spree_apiRubyGems
>= 5.1.0, < 5.1.95.1.9
spree_apiRubyGems
>= 5.2.0, < 5.2.55.2.5

Affected products

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

Patches

4
17e78a91b736

Fixed GHSA-g268-72p7-9j6j Authenticated Insecure Direct Object Refere… (#13423)

https://github.com/spree/spreeDamian LegawiecJan 8, 2026via ghsa
4 files changed · +235 0
  • api/spec/requests/spree/api/v2/storefront/checkout_spec.rb+98 0 modified
    @@ -489,6 +489,104 @@
         end
       end
     
    +  describe 'checkout#update address ownership validation (IDOR protection)' do
    +    let!(:state) { create(:state) }
    +    let!(:country) { state.country }
    +    let(:other_user) { create(:user_with_addresses) }
    +    let(:other_user_address) { other_user.bill_address }
    +    let(:execute) { patch '/api/v2/storefront/checkout', params: params, headers: headers }
    +
    +    context 'as a signed in user' do
    +      include_context 'creates order with line item'
    +
    +      context 'when attempting to use another user\'s address via bill_address_attributes' do
    +        let(:params) do
    +          {
    +            order: {
    +              bill_address_attributes: { id: other_user_address.id }
    +            }
    +          }
    +        end
    +
    +        before { execute }
    +
    +        it 'returns 422 error' do
    +          expect(response.status).to eq(422)
    +        end
    +
    +        it 'does not associate the other user\'s address with the order' do
    +          expect(order.reload.bill_address_id).not_to eq(other_user_address.id)
    +        end
    +
    +        it 'returns an error message' do
    +          expect(json_response['error']).to be_present
    +        end
    +      end
    +
    +      context 'when attempting to use another user\'s address via ship_address_attributes' do
    +        let(:params) do
    +          {
    +            order: {
    +              ship_address_attributes: { id: other_user_address.id }
    +            }
    +          }
    +        end
    +
    +        before { execute }
    +
    +        it 'returns 422 error' do
    +          expect(response.status).to eq(422)
    +        end
    +
    +        it 'does not associate the other user\'s address with the order' do
    +          expect(order.reload.ship_address_id).not_to eq(other_user_address.id)
    +        end
    +      end
    +
    +      context 'when using own address' do
    +        let(:user) { create(:user_with_addresses) }
    +        let(:user_address) { user.bill_address }
    +        let(:params) do
    +          {
    +            order: {
    +              bill_address_attributes: { id: user_address.id }
    +            }
    +          }
    +        end
    +
    +        before { execute }
    +
    +        it 'returns success' do
    +          expect(response.status).to eq(200)
    +        end
    +      end
    +    end
    +
    +    context 'as a guest user' do
    +      include_context 'creates guest order with guest token'
    +
    +      context 'when attempting to use another user\'s address' do
    +        let(:params) do
    +          {
    +            order: {
    +              bill_address_attributes: { id: other_user_address.id }
    +            }
    +          }
    +        end
    +
    +        before { execute }
    +
    +        it 'returns 422 error' do
    +          expect(response.status).to eq(422)
    +        end
    +
    +        it 'does not associate the other user\'s address with the order' do
    +          expect(order.reload.bill_address_id).not_to eq(other_user_address.id)
    +        end
    +      end
    +    end
    +  end
    +
       describe 'checkout#add_store_credit' do
         let(:order_total) { 500.00 }
         let(:params) { { order_token: order.token } }
    
  • core/app/services/spree/checkout/update.rb+24 0 modified
    @@ -5,6 +5,10 @@ class Update
           include Spree::Addresses::Helper
     
           def call(order:, params:, permitted_attributes:, request_env:)
    +        # Validate address ownership to prevent IDOR attacks
    +        address_ownership_error = validate_address_ownership(order, params)
    +        return failure(order, address_ownership_error) if address_ownership_error
    +
             ship_changed = address_with_country_iso_present?(params, 'ship')
             bill_changed = address_with_country_iso_present?(params, 'bill')
             params[:order][:ship_address_attributes] = replace_country_iso_with_id(params[:order][:ship_address_attributes]) if ship_changed
    @@ -26,6 +30,26 @@ def call(order:, params:, permitted_attributes:, request_env:)
     
           private
     
    +      def validate_address_ownership(order, params)
    +        return nil unless params[:order]
    +
    +        %w[bill ship].each do |address_kind|
    +          address_id = params[:order].dig("#{address_kind}_address_attributes".to_sym, :id)
    +          next unless address_id
    +
    +          address = Spree::Address.find_by(id: address_id)
    +          next unless address
    +
    +          # Allow if address has no user (guest address) or belongs to the order's user
    +          next if address.user_id.nil?
    +          next if order.user_id.present? && address.user_id == order.user_id
    +
    +          return Spree.t(:address_not_owned_by_user)
    +        end
    +
    +        nil
    +      end
    +
           def address_with_country_iso_present?(params, address_kind = 'ship')
             return false unless params.dig(:order, "#{address_kind}_address_attributes".to_sym, :country_iso)
             return false if params.dig(:order, "#{address_kind}_address_attributes".to_sym, :country_id)
    
  • core/config/locales/en.yml+1 0 modified
    @@ -608,6 +608,7 @@ en:
           successfully_updated: Updated successfully
           unsuccessfully_saved: There was an error while trying to save your address.
           unsuccessfully_updated: There was an update while trying to update your address.
    +    address_not_owned_by_user: The specified address does not belong to this user.
         addresses: Addresses
         adjustable: Adjustable
         adjustment: Adjustment
    
  • core/spec/services/spree/checkout/update_spec.rb+112 0 modified
    @@ -203,6 +203,118 @@
         end
       end
     
    +  describe 'address ownership validation' do
    +    let(:user) { create(:user_with_addresses) }
    +    let(:other_user) { create(:user_with_addresses) }
    +    let(:order) { create(:order_with_line_items, user: user, state: 'address') }
    +    let(:permitted_attributes) do
    +      Spree::PermittedAttributes.checkout_attributes + [
    +        bill_address_attributes: Spree::PermittedAttributes.address_attributes,
    +        ship_address_attributes: Spree::PermittedAttributes.address_attributes
    +      ]
    +    end
    +
    +    context 'when bill_address_attributes contains id of another user address' do
    +      let(:other_user_address) { other_user.bill_address }
    +      let(:order_params) do
    +        ActionController::Parameters.new(
    +          order: {
    +            bill_address_attributes: { id: other_user_address.id }
    +          }
    +        )
    +      end
    +
    +      it 'returns failure' do
    +        result = described_class.call(order: order, params: order_params, permitted_attributes: permitted_attributes, request_env: nil)
    +        expect(result).to be_failure
    +      end
    +
    +      it 'does not associate the other user address with the order' do
    +        described_class.call(order: order, params: order_params, permitted_attributes: permitted_attributes, request_env: nil)
    +        expect(order.reload.bill_address_id).not_to eq other_user_address.id
    +      end
    +    end
    +
    +    context 'when ship_address_attributes contains id of another user address' do
    +      let(:other_user_address) { other_user.ship_address }
    +      let(:order_params) do
    +        ActionController::Parameters.new(
    +          order: {
    +            ship_address_attributes: { id: other_user_address.id }
    +          }
    +        )
    +      end
    +
    +      it 'returns failure' do
    +        result = described_class.call(order: order, params: order_params, permitted_attributes: permitted_attributes, request_env: nil)
    +        expect(result).to be_failure
    +      end
    +
    +      it 'does not associate the other user address with the order' do
    +        described_class.call(order: order, params: order_params, permitted_attributes: permitted_attributes, request_env: nil)
    +        expect(order.reload.ship_address_id).not_to eq other_user_address.id
    +      end
    +    end
    +
    +    context 'when address_attributes contains id of the same user address' do
    +      let(:user_address) { create(:address, user: user) }
    +      let(:order_params) do
    +        ActionController::Parameters.new(
    +          order: {
    +            bill_address_attributes: { id: user_address.id }
    +          }
    +        )
    +      end
    +
    +      it 'returns success' do
    +        result = described_class.call(order: order, params: order_params, permitted_attributes: permitted_attributes, request_env: nil)
    +        expect(result).to be_success
    +      end
    +    end
    +
    +    context 'when address_attributes contains id of address with no user' do
    +      let(:guest_address) { create(:address, user: nil) }
    +      let(:order_params) do
    +        ActionController::Parameters.new(
    +          order: {
    +            bill_address_attributes: { id: guest_address.id }
    +          }
    +        )
    +      end
    +
    +      it 'returns success' do
    +        result = described_class.call(order: order, params: order_params, permitted_attributes: permitted_attributes, request_env: nil)
    +        expect(result).to be_success
    +      end
    +    end
    +
    +    context 'when address_attributes does not contain id' do
    +      let(:state) { create(:state) }
    +      let(:country) { state.country }
    +      let(:order_params) do
    +        ActionController::Parameters.new(
    +          order: {
    +            bill_address_attributes: {
    +              firstname: 'John',
    +              lastname: 'Doe',
    +              address1: '123 Main St',
    +              city: 'Anytown',
    +              zipcode: '12345',
    +              country_iso: country.iso,
    +              state_id: state.id,
    +              phone: '555-1234'
    +            }
    +          }
    +        )
    +      end
    +
    +      it 'returns success' do
    +        result = described_class.call(order: order, params: order_params, permitted_attributes: permitted_attributes, request_env: nil)
    +        expect(result).to be_success
    +      end
    +    end
    +  end
    +
       describe 'update selected shipping rate' do
         let(:update_service) { described_class.new }
         let(:order) { create(:order_with_line_items) }
    
02acabdce2c5

Fixed GHSA-g268-72p7-9j6j Authenticated Insecure Direct Object Refere… (#13423)

https://github.com/spree/spreeDamian LegawiecJan 8, 2026via ghsa
4 files changed · +235 0
  • api/spec/requests/spree/api/v2/storefront/checkout_spec.rb+98 0 modified
    @@ -489,6 +489,104 @@
         end
       end
     
    +  describe 'checkout#update address ownership validation (IDOR protection)' do
    +    let!(:state) { create(:state) }
    +    let!(:country) { state.country }
    +    let(:other_user) { create(:user_with_addresses) }
    +    let(:other_user_address) { other_user.bill_address }
    +    let(:execute) { patch '/api/v2/storefront/checkout', params: params, headers: headers }
    +
    +    context 'as a signed in user' do
    +      include_context 'creates order with line item'
    +
    +      context 'when attempting to use another user\'s address via bill_address_attributes' do
    +        let(:params) do
    +          {
    +            order: {
    +              bill_address_attributes: { id: other_user_address.id }
    +            }
    +          }
    +        end
    +
    +        before { execute }
    +
    +        it 'returns 422 error' do
    +          expect(response.status).to eq(422)
    +        end
    +
    +        it 'does not associate the other user\'s address with the order' do
    +          expect(order.reload.bill_address_id).not_to eq(other_user_address.id)
    +        end
    +
    +        it 'returns an error message' do
    +          expect(json_response['error']).to be_present
    +        end
    +      end
    +
    +      context 'when attempting to use another user\'s address via ship_address_attributes' do
    +        let(:params) do
    +          {
    +            order: {
    +              ship_address_attributes: { id: other_user_address.id }
    +            }
    +          }
    +        end
    +
    +        before { execute }
    +
    +        it 'returns 422 error' do
    +          expect(response.status).to eq(422)
    +        end
    +
    +        it 'does not associate the other user\'s address with the order' do
    +          expect(order.reload.ship_address_id).not_to eq(other_user_address.id)
    +        end
    +      end
    +
    +      context 'when using own address' do
    +        let(:user) { create(:user_with_addresses) }
    +        let(:user_address) { user.bill_address }
    +        let(:params) do
    +          {
    +            order: {
    +              bill_address_attributes: { id: user_address.id }
    +            }
    +          }
    +        end
    +
    +        before { execute }
    +
    +        it 'returns success' do
    +          expect(response.status).to eq(200)
    +        end
    +      end
    +    end
    +
    +    context 'as a guest user' do
    +      include_context 'creates guest order with guest token'
    +
    +      context 'when attempting to use another user\'s address' do
    +        let(:params) do
    +          {
    +            order: {
    +              bill_address_attributes: { id: other_user_address.id }
    +            }
    +          }
    +        end
    +
    +        before { execute }
    +
    +        it 'returns 422 error' do
    +          expect(response.status).to eq(422)
    +        end
    +
    +        it 'does not associate the other user\'s address with the order' do
    +          expect(order.reload.bill_address_id).not_to eq(other_user_address.id)
    +        end
    +      end
    +    end
    +  end
    +
       describe 'checkout#add_store_credit' do
         let(:order_total) { 500.00 }
         let(:params) { { order_token: order.token } }
    
  • core/app/services/spree/checkout/update.rb+24 0 modified
    @@ -5,6 +5,10 @@ class Update
           include Spree::Addresses::Helper
     
           def call(order:, params:, permitted_attributes:, request_env:)
    +        # Validate address ownership to prevent IDOR attacks
    +        address_ownership_error = validate_address_ownership(order, params)
    +        return failure(order, address_ownership_error) if address_ownership_error
    +
             ship_changed = address_with_country_iso_present?(params, 'ship')
             bill_changed = address_with_country_iso_present?(params, 'bill')
             params[:order][:ship_address_attributes] = replace_country_iso_with_id(params[:order][:ship_address_attributes]) if ship_changed
    @@ -26,6 +30,26 @@ def call(order:, params:, permitted_attributes:, request_env:)
     
           private
     
    +      def validate_address_ownership(order, params)
    +        return nil unless params[:order]
    +
    +        %w[bill ship].each do |address_kind|
    +          address_id = params[:order].dig("#{address_kind}_address_attributes".to_sym, :id)
    +          next unless address_id
    +
    +          address = Spree::Address.find_by(id: address_id)
    +          next unless address
    +
    +          # Allow if address has no user (guest address) or belongs to the order's user
    +          next if address.user_id.nil?
    +          next if order.user_id.present? && address.user_id == order.user_id
    +
    +          return Spree.t(:address_not_owned_by_user)
    +        end
    +
    +        nil
    +      end
    +
           def address_with_country_iso_present?(params, address_kind = 'ship')
             return false unless params.dig(:order, "#{address_kind}_address_attributes".to_sym, :country_iso)
             return false if params.dig(:order, "#{address_kind}_address_attributes".to_sym, :country_id)
    
  • core/config/locales/en.yml+1 0 modified
    @@ -667,6 +667,7 @@ en:
           successfully_updated: Updated successfully
           unsuccessfully_saved: There was an error while trying to save your address.
           unsuccessfully_updated: There was an update while trying to update your address.
    +    address_not_owned_by_user: The specified address does not belong to this user.
         address_settings: Address settings
         addresses: Addresses
         adjustable: Adjustable
    
  • core/spec/services/spree/checkout/update_spec.rb+112 0 modified
    @@ -203,6 +203,118 @@
         end
       end
     
    +  describe 'address ownership validation' do
    +    let(:user) { create(:user_with_addresses) }
    +    let(:other_user) { create(:user_with_addresses) }
    +    let(:order) { create(:order_with_line_items, user: user, state: 'address') }
    +    let(:permitted_attributes) do
    +      Spree::PermittedAttributes.checkout_attributes + [
    +        bill_address_attributes: Spree::PermittedAttributes.address_attributes,
    +        ship_address_attributes: Spree::PermittedAttributes.address_attributes
    +      ]
    +    end
    +
    +    context 'when bill_address_attributes contains id of another user address' do
    +      let(:other_user_address) { other_user.bill_address }
    +      let(:order_params) do
    +        ActionController::Parameters.new(
    +          order: {
    +            bill_address_attributes: { id: other_user_address.id }
    +          }
    +        )
    +      end
    +
    +      it 'returns failure' do
    +        result = described_class.call(order: order, params: order_params, permitted_attributes: permitted_attributes, request_env: nil)
    +        expect(result).to be_failure
    +      end
    +
    +      it 'does not associate the other user address with the order' do
    +        described_class.call(order: order, params: order_params, permitted_attributes: permitted_attributes, request_env: nil)
    +        expect(order.reload.bill_address_id).not_to eq other_user_address.id
    +      end
    +    end
    +
    +    context 'when ship_address_attributes contains id of another user address' do
    +      let(:other_user_address) { other_user.ship_address }
    +      let(:order_params) do
    +        ActionController::Parameters.new(
    +          order: {
    +            ship_address_attributes: { id: other_user_address.id }
    +          }
    +        )
    +      end
    +
    +      it 'returns failure' do
    +        result = described_class.call(order: order, params: order_params, permitted_attributes: permitted_attributes, request_env: nil)
    +        expect(result).to be_failure
    +      end
    +
    +      it 'does not associate the other user address with the order' do
    +        described_class.call(order: order, params: order_params, permitted_attributes: permitted_attributes, request_env: nil)
    +        expect(order.reload.ship_address_id).not_to eq other_user_address.id
    +      end
    +    end
    +
    +    context 'when address_attributes contains id of the same user address' do
    +      let(:user_address) { create(:address, user: user) }
    +      let(:order_params) do
    +        ActionController::Parameters.new(
    +          order: {
    +            bill_address_attributes: { id: user_address.id }
    +          }
    +        )
    +      end
    +
    +      it 'returns success' do
    +        result = described_class.call(order: order, params: order_params, permitted_attributes: permitted_attributes, request_env: nil)
    +        expect(result).to be_success
    +      end
    +    end
    +
    +    context 'when address_attributes contains id of address with no user' do
    +      let(:guest_address) { create(:address, user: nil) }
    +      let(:order_params) do
    +        ActionController::Parameters.new(
    +          order: {
    +            bill_address_attributes: { id: guest_address.id }
    +          }
    +        )
    +      end
    +
    +      it 'returns success' do
    +        result = described_class.call(order: order, params: order_params, permitted_attributes: permitted_attributes, request_env: nil)
    +        expect(result).to be_success
    +      end
    +    end
    +
    +    context 'when address_attributes does not contain id' do
    +      let(:state) { create(:state) }
    +      let(:country) { state.country }
    +      let(:order_params) do
    +        ActionController::Parameters.new(
    +          order: {
    +            bill_address_attributes: {
    +              firstname: 'John',
    +              lastname: 'Doe',
    +              address1: '123 Main St',
    +              city: 'Anytown',
    +              zipcode: '12345',
    +              country_iso: country.iso,
    +              state_id: state.id,
    +              phone: '555-1234'
    +            }
    +          }
    +        )
    +      end
    +
    +      it 'returns success' do
    +        result = described_class.call(order: order, params: order_params, permitted_attributes: permitted_attributes, request_env: nil)
    +        expect(result).to be_success
    +      end
    +    end
    +  end
    +
       describe 'update selected shipping rate' do
         let(:update_service) { described_class.new }
         let(:order) { create(:order_with_line_items) }
    
b409c0fd327e

Fixed GHSA-g268-72p7-9j6j Authenticated Insecure Direct Object Refere… (#13423)

https://github.com/spree/spreeDamian LegawiecJan 8, 2026via ghsa
4 files changed · +235 0
  • api/spec/requests/spree/api/v2/storefront/checkout_spec.rb+98 0 modified
    @@ -450,6 +450,104 @@
         end
       end
     
    +  describe 'checkout#update address ownership validation (IDOR protection)' do
    +    let!(:state) { create(:state) }
    +    let!(:country) { state.country }
    +    let(:other_user) { create(:user_with_addresses) }
    +    let(:other_user_address) { other_user.bill_address }
    +    let(:execute) { patch '/api/v2/storefront/checkout', params: params, headers: headers }
    +
    +    context 'as a signed in user' do
    +      include_context 'creates order with line item'
    +
    +      context 'when attempting to use another user\'s address via bill_address_attributes' do
    +        let(:params) do
    +          {
    +            order: {
    +              bill_address_attributes: { id: other_user_address.id }
    +            }
    +          }
    +        end
    +
    +        before { execute }
    +
    +        it 'returns 422 error' do
    +          expect(response.status).to eq(422)
    +        end
    +
    +        it 'does not associate the other user\'s address with the order' do
    +          expect(order.reload.bill_address_id).not_to eq(other_user_address.id)
    +        end
    +
    +        it 'returns an error message' do
    +          expect(json_response['error']).to be_present
    +        end
    +      end
    +
    +      context 'when attempting to use another user\'s address via ship_address_attributes' do
    +        let(:params) do
    +          {
    +            order: {
    +              ship_address_attributes: { id: other_user_address.id }
    +            }
    +          }
    +        end
    +
    +        before { execute }
    +
    +        it 'returns 422 error' do
    +          expect(response.status).to eq(422)
    +        end
    +
    +        it 'does not associate the other user\'s address with the order' do
    +          expect(order.reload.ship_address_id).not_to eq(other_user_address.id)
    +        end
    +      end
    +
    +      context 'when using own address' do
    +        let(:user) { create(:user_with_addresses) }
    +        let(:user_address) { user.bill_address }
    +        let(:params) do
    +          {
    +            order: {
    +              bill_address_attributes: { id: user_address.id }
    +            }
    +          }
    +        end
    +
    +        before { execute }
    +
    +        it 'returns success' do
    +          expect(response.status).to eq(200)
    +        end
    +      end
    +    end
    +
    +    context 'as a guest user' do
    +      include_context 'creates guest order with guest token'
    +
    +      context 'when attempting to use another user\'s address' do
    +        let(:params) do
    +          {
    +            order: {
    +              bill_address_attributes: { id: other_user_address.id }
    +            }
    +          }
    +        end
    +
    +        before { execute }
    +
    +        it 'returns 422 error' do
    +          expect(response.status).to eq(422)
    +        end
    +
    +        it 'does not associate the other user\'s address with the order' do
    +          expect(order.reload.bill_address_id).not_to eq(other_user_address.id)
    +        end
    +      end
    +    end
    +  end
    +
       describe 'checkout#add_store_credit' do
         let(:order_total) { 500.00 }
         let(:params) { { order_token: order.token } }
    
  • core/app/services/spree/checkout/update.rb+24 0 modified
    @@ -5,6 +5,10 @@ class Update
           include Spree::Addresses::Helper
     
           def call(order:, params:, permitted_attributes:, request_env:)
    +        # Validate address ownership to prevent IDOR attacks
    +        address_ownership_error = validate_address_ownership(order, params)
    +        return failure(order, address_ownership_error) if address_ownership_error
    +
             ship_changed = address_with_country_iso_present?(params, 'ship')
             bill_changed = address_with_country_iso_present?(params, 'bill')
             params[:order][:ship_address_attributes] = replace_country_iso_with_id(params[:order][:ship_address_attributes]) if ship_changed
    @@ -18,6 +22,26 @@ def call(order:, params:, permitted_attributes:, request_env:)
     
           private
     
    +      def validate_address_ownership(order, params)
    +        return nil unless params[:order]
    +
    +        %w[bill ship].each do |address_kind|
    +          address_id = params[:order].dig("#{address_kind}_address_attributes".to_sym, :id)
    +          next unless address_id
    +
    +          address = Spree::Address.find_by(id: address_id)
    +          next unless address
    +
    +          # Allow if address has no user (guest address) or belongs to the order's user
    +          next if address.user_id.nil?
    +          next if order.user_id.present? && address.user_id == order.user_id
    +
    +          return Spree.t(:address_not_owned_by_user)
    +        end
    +
    +        nil
    +      end
    +
           def address_with_country_iso_present?(params, address_kind = 'ship')
             return false unless params.dig(:order, "#{address_kind}_address_attributes".to_sym, :country_iso)
             return false if params.dig(:order, "#{address_kind}_address_attributes".to_sym, :country_id)
    
  • core/config/locales/en.yml+1 0 modified
    @@ -548,6 +548,7 @@ en:
           successfully_updated: "Updated successfully"
           unsuccessfully_updated: "There was an update while trying to update your address."
           save: "Save"
    +    address_not_owned_by_user: The specified address does not belong to this user.
         adjustable: Adjustable
         adjustment: Adjustment
         adjustment_amount: Amount
    
  • core/spec/services/spree/checkout/update_spec.rb+112 0 modified
    @@ -187,6 +187,118 @@
         end
       end
     
    +  describe 'address ownership validation' do
    +    let(:user) { create(:user_with_addresses) }
    +    let(:other_user) { create(:user_with_addresses) }
    +    let(:order) { create(:order_with_line_items, user: user, state: 'address') }
    +    let(:permitted_attributes) do
    +      Spree::PermittedAttributes.checkout_attributes + [
    +        bill_address_attributes: Spree::PermittedAttributes.address_attributes,
    +        ship_address_attributes: Spree::PermittedAttributes.address_attributes
    +      ]
    +    end
    +
    +    context 'when bill_address_attributes contains id of another user address' do
    +      let(:other_user_address) { other_user.bill_address }
    +      let(:order_params) do
    +        ActionController::Parameters.new(
    +          order: {
    +            bill_address_attributes: { id: other_user_address.id }
    +          }
    +        )
    +      end
    +
    +      it 'returns failure' do
    +        result = described_class.call(order: order, params: order_params, permitted_attributes: permitted_attributes, request_env: nil)
    +        expect(result).to be_failure
    +      end
    +
    +      it 'does not associate the other user address with the order' do
    +        described_class.call(order: order, params: order_params, permitted_attributes: permitted_attributes, request_env: nil)
    +        expect(order.reload.bill_address_id).not_to eq other_user_address.id
    +      end
    +    end
    +
    +    context 'when ship_address_attributes contains id of another user address' do
    +      let(:other_user_address) { other_user.ship_address }
    +      let(:order_params) do
    +        ActionController::Parameters.new(
    +          order: {
    +            ship_address_attributes: { id: other_user_address.id }
    +          }
    +        )
    +      end
    +
    +      it 'returns failure' do
    +        result = described_class.call(order: order, params: order_params, permitted_attributes: permitted_attributes, request_env: nil)
    +        expect(result).to be_failure
    +      end
    +
    +      it 'does not associate the other user address with the order' do
    +        described_class.call(order: order, params: order_params, permitted_attributes: permitted_attributes, request_env: nil)
    +        expect(order.reload.ship_address_id).not_to eq other_user_address.id
    +      end
    +    end
    +
    +    context 'when address_attributes contains id of the same user address' do
    +      let(:user_address) { create(:address, user: user) }
    +      let(:order_params) do
    +        ActionController::Parameters.new(
    +          order: {
    +            bill_address_attributes: { id: user_address.id }
    +          }
    +        )
    +      end
    +
    +      it 'returns success' do
    +        result = described_class.call(order: order, params: order_params, permitted_attributes: permitted_attributes, request_env: nil)
    +        expect(result).to be_success
    +      end
    +    end
    +
    +    context 'when address_attributes contains id of address with no user' do
    +      let(:guest_address) { create(:address, user: nil) }
    +      let(:order_params) do
    +        ActionController::Parameters.new(
    +          order: {
    +            bill_address_attributes: { id: guest_address.id }
    +          }
    +        )
    +      end
    +
    +      it 'returns success' do
    +        result = described_class.call(order: order, params: order_params, permitted_attributes: permitted_attributes, request_env: nil)
    +        expect(result).to be_success
    +      end
    +    end
    +
    +    context 'when address_attributes does not contain id' do
    +      let(:state) { create(:state) }
    +      let(:country) { state.country }
    +      let(:order_params) do
    +        ActionController::Parameters.new(
    +          order: {
    +            bill_address_attributes: {
    +              firstname: 'John',
    +              lastname: 'Doe',
    +              address1: '123 Main St',
    +              city: 'Anytown',
    +              zipcode: '12345',
    +              country_iso: country.iso,
    +              state_id: state.id,
    +              phone: '555-1234'
    +            }
    +          }
    +        )
    +      end
    +
    +      it 'returns success' do
    +        result = described_class.call(order: order, params: order_params, permitted_attributes: permitted_attributes, request_env: nil)
    +        expect(result).to be_success
    +      end
    +    end
    +  end
    +
       describe 'update selected shipping rate' do
         let(:update_service) { described_class.new }
         let(:order) { create(:order_with_line_items) }
    
d3f961c442e0

Fixed GHSA-g268-72p7-9j6j Authenticated Insecure Direct Object Refere… (#13423)

https://github.com/spree/spreeDamian LegawiecJan 8, 2026via ghsa
4 files changed · +235 0
  • api/spec/requests/spree/api/v2/storefront/checkout_spec.rb+98 0 modified
    @@ -489,6 +489,104 @@
         end
       end
     
    +  describe 'checkout#update address ownership validation (IDOR protection)' do
    +    let!(:state) { create(:state) }
    +    let!(:country) { state.country }
    +    let(:other_user) { create(:user_with_addresses) }
    +    let(:other_user_address) { other_user.bill_address }
    +    let(:execute) { patch '/api/v2/storefront/checkout', params: params, headers: headers }
    +
    +    context 'as a signed in user' do
    +      include_context 'creates order with line item'
    +
    +      context 'when attempting to use another user\'s address via bill_address_attributes' do
    +        let(:params) do
    +          {
    +            order: {
    +              bill_address_attributes: { id: other_user_address.id }
    +            }
    +          }
    +        end
    +
    +        before { execute }
    +
    +        it 'returns 422 error' do
    +          expect(response.status).to eq(422)
    +        end
    +
    +        it 'does not associate the other user\'s address with the order' do
    +          expect(order.reload.bill_address_id).not_to eq(other_user_address.id)
    +        end
    +
    +        it 'returns an error message' do
    +          expect(json_response['error']).to be_present
    +        end
    +      end
    +
    +      context 'when attempting to use another user\'s address via ship_address_attributes' do
    +        let(:params) do
    +          {
    +            order: {
    +              ship_address_attributes: { id: other_user_address.id }
    +            }
    +          }
    +        end
    +
    +        before { execute }
    +
    +        it 'returns 422 error' do
    +          expect(response.status).to eq(422)
    +        end
    +
    +        it 'does not associate the other user\'s address with the order' do
    +          expect(order.reload.ship_address_id).not_to eq(other_user_address.id)
    +        end
    +      end
    +
    +      context 'when using own address' do
    +        let(:user) { create(:user_with_addresses) }
    +        let(:user_address) { user.bill_address }
    +        let(:params) do
    +          {
    +            order: {
    +              bill_address_attributes: { id: user_address.id }
    +            }
    +          }
    +        end
    +
    +        before { execute }
    +
    +        it 'returns success' do
    +          expect(response.status).to eq(200)
    +        end
    +      end
    +    end
    +
    +    context 'as a guest user' do
    +      include_context 'creates guest order with guest token'
    +
    +      context 'when attempting to use another user\'s address' do
    +        let(:params) do
    +          {
    +            order: {
    +              bill_address_attributes: { id: other_user_address.id }
    +            }
    +          }
    +        end
    +
    +        before { execute }
    +
    +        it 'returns 422 error' do
    +          expect(response.status).to eq(422)
    +        end
    +
    +        it 'does not associate the other user\'s address with the order' do
    +          expect(order.reload.bill_address_id).not_to eq(other_user_address.id)
    +        end
    +      end
    +    end
    +  end
    +
       describe 'checkout#add_store_credit' do
         let(:order_total) { 500.00 }
         let(:params) { { order_token: order.token } }
    
  • core/app/services/spree/checkout/update.rb+24 0 modified
    @@ -5,6 +5,10 @@ class Update
           include Spree::Addresses::Helper
     
           def call(order:, params:, permitted_attributes:, request_env:)
    +        # Validate address ownership to prevent IDOR attacks
    +        address_ownership_error = validate_address_ownership(order, params)
    +        return failure(order, address_ownership_error) if address_ownership_error
    +
             ship_changed = address_with_country_iso_present?(params, 'ship')
             bill_changed = address_with_country_iso_present?(params, 'bill')
             params[:order][:ship_address_attributes] = replace_country_iso_with_id(params[:order][:ship_address_attributes]) if ship_changed
    @@ -26,6 +30,26 @@ def call(order:, params:, permitted_attributes:, request_env:)
     
           private
     
    +      def validate_address_ownership(order, params)
    +        return nil unless params[:order]
    +
    +        %w[bill ship].each do |address_kind|
    +          address_id = params[:order].dig("#{address_kind}_address_attributes".to_sym, :id)
    +          next unless address_id
    +
    +          address = Spree::Address.find_by(id: address_id)
    +          next unless address
    +
    +          # Allow if address has no user (guest address) or belongs to the order's user
    +          next if address.user_id.nil?
    +          next if order.user_id.present? && address.user_id == order.user_id
    +
    +          return Spree.t(:address_not_owned_by_user)
    +        end
    +
    +        nil
    +      end
    +
           def address_with_country_iso_present?(params, address_kind = 'ship')
             return false unless params.dig(:order, "#{address_kind}_address_attributes".to_sym, :country_iso)
             return false if params.dig(:order, "#{address_kind}_address_attributes".to_sym, :country_id)
    
  • core/config/locales/en.yml+1 0 modified
    @@ -642,6 +642,7 @@ en:
           successfully_updated: Updated successfully
           unsuccessfully_saved: There was an error while trying to save your address.
           unsuccessfully_updated: There was an update while trying to update your address.
    +    address_not_owned_by_user: The specified address does not belong to this user.
         address_settings: Address settings
         addresses: Addresses
         adjustable: Adjustable
    
  • core/spec/services/spree/checkout/update_spec.rb+112 0 modified
    @@ -203,6 +203,118 @@
         end
       end
     
    +  describe 'address ownership validation' do
    +    let(:user) { create(:user_with_addresses) }
    +    let(:other_user) { create(:user_with_addresses) }
    +    let(:order) { create(:order_with_line_items, user: user, state: 'address') }
    +    let(:permitted_attributes) do
    +      Spree::PermittedAttributes.checkout_attributes + [
    +        bill_address_attributes: Spree::PermittedAttributes.address_attributes,
    +        ship_address_attributes: Spree::PermittedAttributes.address_attributes
    +      ]
    +    end
    +
    +    context 'when bill_address_attributes contains id of another user address' do
    +      let(:other_user_address) { other_user.bill_address }
    +      let(:order_params) do
    +        ActionController::Parameters.new(
    +          order: {
    +            bill_address_attributes: { id: other_user_address.id }
    +          }
    +        )
    +      end
    +
    +      it 'returns failure' do
    +        result = described_class.call(order: order, params: order_params, permitted_attributes: permitted_attributes, request_env: nil)
    +        expect(result).to be_failure
    +      end
    +
    +      it 'does not associate the other user address with the order' do
    +        described_class.call(order: order, params: order_params, permitted_attributes: permitted_attributes, request_env: nil)
    +        expect(order.reload.bill_address_id).not_to eq other_user_address.id
    +      end
    +    end
    +
    +    context 'when ship_address_attributes contains id of another user address' do
    +      let(:other_user_address) { other_user.ship_address }
    +      let(:order_params) do
    +        ActionController::Parameters.new(
    +          order: {
    +            ship_address_attributes: { id: other_user_address.id }
    +          }
    +        )
    +      end
    +
    +      it 'returns failure' do
    +        result = described_class.call(order: order, params: order_params, permitted_attributes: permitted_attributes, request_env: nil)
    +        expect(result).to be_failure
    +      end
    +
    +      it 'does not associate the other user address with the order' do
    +        described_class.call(order: order, params: order_params, permitted_attributes: permitted_attributes, request_env: nil)
    +        expect(order.reload.ship_address_id).not_to eq other_user_address.id
    +      end
    +    end
    +
    +    context 'when address_attributes contains id of the same user address' do
    +      let(:user_address) { create(:address, user: user) }
    +      let(:order_params) do
    +        ActionController::Parameters.new(
    +          order: {
    +            bill_address_attributes: { id: user_address.id }
    +          }
    +        )
    +      end
    +
    +      it 'returns success' do
    +        result = described_class.call(order: order, params: order_params, permitted_attributes: permitted_attributes, request_env: nil)
    +        expect(result).to be_success
    +      end
    +    end
    +
    +    context 'when address_attributes contains id of address with no user' do
    +      let(:guest_address) { create(:address, user: nil) }
    +      let(:order_params) do
    +        ActionController::Parameters.new(
    +          order: {
    +            bill_address_attributes: { id: guest_address.id }
    +          }
    +        )
    +      end
    +
    +      it 'returns success' do
    +        result = described_class.call(order: order, params: order_params, permitted_attributes: permitted_attributes, request_env: nil)
    +        expect(result).to be_success
    +      end
    +    end
    +
    +    context 'when address_attributes does not contain id' do
    +      let(:state) { create(:state) }
    +      let(:country) { state.country }
    +      let(:order_params) do
    +        ActionController::Parameters.new(
    +          order: {
    +            bill_address_attributes: {
    +              firstname: 'John',
    +              lastname: 'Doe',
    +              address1: '123 Main St',
    +              city: 'Anytown',
    +              zipcode: '12345',
    +              country_iso: country.iso,
    +              state_id: state.id,
    +              phone: '555-1234'
    +            }
    +          }
    +        )
    +      end
    +
    +      it 'returns success' do
    +        result = described_class.call(order: order, params: order_params, permitted_attributes: permitted_attributes, request_env: nil)
    +        expect(result).to be_success
    +      end
    +    end
    +  end
    +
       describe 'update selected shipping rate' do
         let(:update_service) { described_class.new }
         let(:order) { create(:order_with_line_items) }
    

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.