CVE-2026-94462
Spree: Broken Access Control in `PATCH /api/v3/store/carts/:id/associate` (IDOR)
Spree is an open source e-commerce solution built with Ruby on Rails. From 5.4.0 until 5.4.4 and 5.5.4, PATCH /api/v3/store/carts/:id/associate in Spree::Api::V3::Store::CartsController#associate uses find_cart_for_association to locate a cart by prefixed_id but does not require a cart token or otherwise verify possession of the selected guest cart. An authenticated customer can derive reversible prefixed cart IDs, associate an eligible guest cart with the attacker's account, and receive billing and shipping address data from the cart. Exploitation requires a guest cart with address data on a store that does not require login for checkout, and reassignment can also disrupt the guest's in-progress cart. This issue is fixed in versions 5.4.4 and 5.5.4.
Attack profile
How easy it is to exploit
- Attack vector
- Network
- Complexity
- Low
- Privileges needed
- Low
- User interaction
- None
What an attacker can affect
- Confidentiality
- High
- Integrity
- Low
- Availability
- None
Exploit likelihood
What each source reports
CVE List Score used
7.1
Spree: Broken Access Control in `PATCH /api/v3/store/carts/:id/associate` (IDOR)
Spree is an open source e-commerce solution built with Ruby on Rails. From 5.4.0 until 5.4.4 and 5.5.4, PATCH /api/v3/store/carts/:id/associate in Spree::Api::V3::Store::CartsController#associate uses find_cart_for_association to locate a cart by prefixed_id but does not require a cart token or otherwise verify possession of the selected guest cart. An authenticated customer can derive reversible prefixed cart IDs, associate an eligible guest cart with the attacker's account, and receive billing and shipping address data from the cart. Exploitation requires a guest cart with address data on a store that does not require login for checkout, and reassignment can also disrupt the guest's in-progress cart. This issue is fixed in versions 5.4.4 and 5.5.4.
- Record
CVE-2026-94462- CVSS 3.1
- 7.1 High
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N - Weakness
- CWE-639
- Updated
- 2026-11-02
EUVDEUVD-2026-84794
7.1
Spree is an open source e-commerce solution built with Ruby on Rails. From 5.4.0 until 5.4.4 and 5.5.4, PATCH /api/v3/store/carts/:id/associate in Spree::Api::V3::Store::CartsController#associate uses find_cart_for_association to locate a cart by prefixed_id but does not require a cart token or otherwise verify possession of the selected guest cart. An authenticated customer can derive reversible prefixed cart IDs, associate an eligible guest cart with the attacker's account, and receive billing and shipping address data from the cart. Exploitation requires a guest cart with address data on a store that does not require login for checkout, and reassignment can also disrupt the guest's in-progress cart. This issue is fixed in versions 5.4.4 and 5.5.4.
- Record
EUVD-2026-84794- CVSS 3.1
- 7.1 High
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-4825-p4xm-pcf2
7.1
Spree: Broken Access Control in `PATCH /api/v3/store/carts/:id/associate` (IDOR)
Summary
The Store API v3 endpoint PATCH /api/v3/store/carts/:id/associate binds a guest cart to the authenticated caller without verifying possession of that cart. It locates the cart by prefixed ID only — current_store.carts.where(user: [nil, current_user]).find_by_prefix_id!(params[:id]) — and omits the authorize!(:update, @cart, cart_token) check that every other action in the controller performs via CartResolvable. Because prefixed IDs are a reversible Sqids encoding of the auto-increment primary key (obfuscation, not a token), an authenticated customer can name arbitrary guest cart IDs, take them over, and read the checkout addresses stored on them. This is broken access control / IDOR, reachable by any low-privilege registered user.
Severity
Requires an authenticated store account and depends on target guest carts already carrying an address and not yet being associated, on a store not running in login_required mode. Confidentiality impact is the driver (guest checkout PII); integrity impact is limited and recoverable (cart reassignment + email overwrite on an in-progress cart). Not Critical: the action is gated behind authentication (PR:L, not PR:N) and constrained by cart state, so it is not anonymously exploitable.
Details
Root cause: associate skips the cart-possession check its sibling actions enforce and trusts a guessable identifier as the sole locator.
Entry point. Spree::Api::V3::Store::CartsController#associate (carts_controller.rb:88-96), guarded only by prepend_before_action :require_authentication!, only: [:index, :associate]. That requires the caller be authenticated; it does not tie the request to a specific guest cart.
# spree/api/app/controllers/spree/api/v3/store/carts_controller.rb:88-96
# PATCH /api/v3/store/carts/:id/associate
def associate
@cart = find_cart_for_association
result = Spree.cart_associate_service.call(guest_order: @cart, user: current_user, guest_only: true)
if result.success?
render_cart
else
render_service_error(result.error.to_s)
end
end
Missing check. find_cart_for_association (carts_controller.rb:177-178) resolves any guest cart (user IS NULL) in the store by ID with no authorize!(..., cart_token). Contrast CartResolvable#find_cart!, which binds the token.
# spree/api/app/controllers/spree/api/v3/store/carts_controller.rb:177-178
def find_cart_for_association
current_store.carts.where(user: [nil, current_user]).find_by_prefix_id!(params[:id])
end
Identifier. prefixed_id is "cart_" + SQIDS.encode([id]) with SQIDS = Sqids.new(min_length: 10) (prefixed_id.rb:17,56) — default alphabet, no salt, no blocklist. Sqids is non-cryptographic and reversible, so candidate IDs are derivable offline from sequential primary keys.
# spree/core/app/models/concerns/spree/prefixed_id.rb:17-56
SQIDS = Sqids.new(min_length: 10)
def prefixed_id
return nil unless id.present?
"#{self.class._prefix_id_prefix}_#{Spree::PrefixedId::SQIDS.encode([id])}"
end
Data flow. Spree.cart_associate_service.call(guest_order: @cart, user: current_user, guest_only: true) reassigns the owner and overwrites email, preserving existing addresses via bill_address ||= / ship_address ||=. render_cart then serializes billing_address/shipping_address (first_name, last_name, address1, address2, city, postal_code, phone, company) back to the caller.
PoC
Preconditions: attacker holds an ordinary store account (self-service registration) and the store's publishable key (a front-end credential, present in any headless storefront bundle); one or more guest carts carry checkout addresses; store is not in login_required mode.
- Authenticate:
POST /api/v3/store/auth/login→ attacker JWT. - Derive candidate IDs offline:
"cart_" + Sqids.encode([n])for a range ofn. - For each candidate:
PATCH /api/v3/store/carts/<id>/associatewith the attacker JWT. A hit…
- Record
GHSA-4825-p4xm-pcf2- CVSS 3.1
- 7.1 High
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N - Weakness
- CWE-639
- Updated
- 2026-11-02
NVD
7.1
Spree is an open source e-commerce solution built with Ruby on Rails. From 5.4.0 until 5.4.4 and 5.5.4, PATCH /api/v3/store/carts/:id/associate in Spree::Api::V3::Store::CartsController#associate uses find_cart_for_association to locate a cart by prefixed_id but does not require a cart token or otherwise verify possession of the selected guest cart. An authenticated customer can derive reversible prefixed cart IDs, associate an eligible guest cart with the attacker's account, and receive billing and shipping address data from the cart. Exploitation requires a guest cart with address data on a store that does not require login for checkout, and reassignment can also disrupt the guest's in-progress cart. This issue is fixed in versions 5.4.4 and 5.5.4.
- Record
CVE-2026-94462- CVSS 3.1
- 7.1 High
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N - Weakness
- CWE-639
- Updated
- 2026-11-02
OSV
7.1
Spree: Broken Access Control in `PATCH /api/v3/store/carts/:id/associate` (IDOR)
Spree is an open source e-commerce solution built with Ruby on Rails. From 5.4.0 until 5.4.4 and 5.5.4, PATCH /api/v3/store/carts/:id/associate in Spree::Api::V3::Store::CartsController#associate uses find_cart_for_association to locate a cart by prefixed_id but does not require a cart token or otherwise verify possession of the selected guest cart. An authenticated customer can derive reversible prefixed cart IDs, associate an eligible guest cart with the attacker's account, and receive billing and shipping address data from the cart. Exploitation requires a guest cart with address data on a store that does not require login for checkout, and reassignment can also disrupt the guest's in-progress cart. This issue is fixed in versions 5.4.4 and 5.5.4.
- Record
CVE-2026-94462- CVSS 3.1
- 7.1 High
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N - Weakness
- CWE-639
- Updated
- 2026-11-02
OSVGHSA-4825-p4xm-pcf2
7.1
Spree: Broken Access Control in `PATCH /api/v3/store/carts/:id/associate` (IDOR)
Summary
The Store API v3 endpoint PATCH /api/v3/store/carts/:id/associate binds a guest cart to the authenticated caller without verifying possession of that cart. It locates the cart by prefixed ID only — current_store.carts.where(user: [nil, current_user]).find_by_prefix_id!(params[:id]) — and omits the authorize!(:update, @cart, cart_token) check that every other action in the controller performs via CartResolvable. Because prefixed IDs are a reversible Sqids encoding of the auto-increment primary key (obfuscation, not a token), an authenticated customer can name arbitrary guest cart IDs, take them over, and read the checkout addresses stored on them. This is broken access control / IDOR, reachable by any low-privilege registered user.
Severity
Requires an authenticated store account and depends on target guest carts already carrying an address and not yet being associated, on a store not running in login_required mode. Confidentiality impact is the driver (guest checkout PII); integrity impact is limited and recoverable (cart reassignment + email overwrite on an in-progress cart). Not Critical: the action is gated behind authentication (PR:L, not PR:N) and constrained by cart state, so it is not anonymously exploitable.
Details
Root cause: associate skips the cart-possession check its sibling actions enforce and trusts a guessable identifier as the sole locator.
Entry point. Spree::Api::V3::Store::CartsController#associate (carts_controller.rb:88-96), guarded only by prepend_before_action :require_authentication!, only: [:index, :associate]. That requires the caller be authenticated; it does not tie the request to a specific guest cart.
# spree/api/app/controllers/spree/api/v3/store/carts_controller.rb:88-96
# PATCH /api/v3/store/carts/:id/associate
def associate
@cart = find_cart_for_association
result = Spree.cart_associate_service.call(guest_order: @cart, user: current_user, guest_only: true)
if result.success?
render_cart
else
render_service_error(result.error.to_s)
end
end
Missing check. find_cart_for_association (carts_controller.rb:177-178) resolves any guest cart (user IS NULL) in the store by ID with no authorize!(..., cart_token). Contrast CartResolvable#find_cart!, which binds the token.
# spree/api/app/controllers/spree/api/v3/store/carts_controller.rb:177-178
def find_cart_for_association
current_store.carts.where(user: [nil, current_user]).find_by_prefix_id!(params[:id])
end
Identifier. prefixed_id is "cart_" + SQIDS.encode([id]) with SQIDS = Sqids.new(min_length: 10) (prefixed_id.rb:17,56) — default alphabet, no salt, no blocklist. Sqids is non-cryptographic and reversible, so candidate IDs are derivable offline from sequential primary keys.
# spree/core/app/models/concerns/spree/prefixed_id.rb:17-56
SQIDS = Sqids.new(min_length: 10)
def prefixed_id
return nil unless id.present?
"#{self.class._prefix_id_prefix}_#{Spree::PrefixedId::SQIDS.encode([id])}"
end
Data flow. Spree.cart_associate_service.call(guest_order: @cart, user: current_user, guest_only: true) reassigns the owner and overwrites email, preserving existing addresses via bill_address ||= / ship_address ||=. render_cart then serializes billing_address/shipping_address (first_name, last_name, address1, address2, city, postal_code, phone, company) back to the caller.
PoC
Preconditions: attacker holds an ordinary store account (self-service registration) and the store's publishable key (a front-end credential, present in any headless storefront bundle); one or more guest carts carry checkout addresses; store is not in login_required mode.
- Authenticate:
POST /api/v3/store/auth/login→ attacker JWT. - Derive candidate IDs offline:
"cart_" + Sqids.encode([n])for a range ofn. - For each candidate:
PATCH /api/v3/store/carts/<id>/associatewith the attacker JWT. A hit…
- Record
GHSA-4825-p4xm-pcf2- CVSS 3.1
- 7.1 High
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N - Weakness
- CWE-639
- Updated
- 2026-11-02
Affected products
Affected packages
History
| Recorded | Changed | Sources |
|---|---|---|
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-94462 is a high vulnerability (CVSS 7.1) in Spree, published September 22, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.38%, the 32nd percentile. Fixed in spree_api 5.4.4.
References
github.com/spree/spree/security/advisories/GHSA-4825-p4xm-pcf2
github.com/spree/spree/pull/14314
github.com/spree/spree/commit/8834230a1f47bb5988f23f45dbd162776cf592bd
github.com/spree/spree/commit/af0d1a2d582a60d179de65b7d3ea024cb26426a8
github.com/spree/spree/releases/tag/v5.4.4
github.com/spree/spree/releases/tag/v5.5.4
Left out here: the rest of 2 source descriptions. The JSON record has them.