Spree API has Authenticated Insecure Direct Object Reference (IDOR) via Order Modification
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.
| Package | Affected versions | Patched versions |
|---|---|---|
spree_apiRubyGems | >= 3.7.0, < 4.10.2 | 4.10.2 |
spree_apiRubyGems | >= 5.0.0, < 5.0.7 | 5.0.7 |
spree_apiRubyGems | >= 5.1.0, < 5.1.9 | 5.1.9 |
spree_apiRubyGems | >= 5.2.0, < 5.2.5 | 5.2.5 |
Affected products
1Patches
417e78a91b736Fixed GHSA-g268-72p7-9j6j Authenticated Insecure Direct Object Refere… (#13423)
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) }
02acabdce2c5Fixed GHSA-g268-72p7-9j6j Authenticated Insecure Direct Object Refere… (#13423)
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) }
b409c0fd327eFixed GHSA-g268-72p7-9j6j Authenticated Insecure Direct Object Refere… (#13423)
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) }
d3f961c442e0Fixed GHSA-g268-72p7-9j6j Authenticated Insecure Direct Object Refere… (#13423)
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- github.com/advisories/GHSA-g268-72p7-9j6jghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-22588ghsaADVISORY
- github.com/rubysec/ruby-advisory-db/blob/master/gems/spree_api/CVE-2026-22588.ymlghsaWEB
- github.com/spree/spree/commit/02acabdce2c5f14fd687335b068d901a957a7e72ghsax_refsource_MISCWEB
- github.com/spree/spree/commit/17e78a91b736b49dbea8d1bb1223c284383ee5f3ghsax_refsource_MISCWEB
- github.com/spree/spree/commit/b409c0fd327e7ce37f63238894670d07079eefe8ghsax_refsource_MISCWEB
- github.com/spree/spree/commit/d3f961c442e0015661535cbd6eb22475f76d2dc7ghsax_refsource_MISCWEB
- github.com/spree/spree/security/advisories/GHSA-g268-72p7-9j6jghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.