See how spree compares to other vendors in security performance
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 — currentstore.carts.where(user: [nil, currentuser]).findbyprefixid!(params[:id]) — and omits the authorize!(:update, @cart, carttoken) 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 loginrequired 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 (cartscontroller.rb:88-96), guarded only by prependbeforeaction :requireauthentication!, only: [:index, :associate]. That requires the caller be authenticated; it does not tie the request to a specific guest cart.
ruby spree/api/app/controllers/spree/api/v3/store/cartscontroller.rb:88-96 PATCH /api/v3/store/carts/:id/associate def associate @cart = findcartforassociation
result = Spree.cartassociateservice.call(guestorder: @cart, user: currentuser, guestonly: true)
if result.success? rendercart else renderserviceerror(result.error.tos) end end
Missing check. findcartforassociation (cartscontroller.rb:177-178) resolves any guest cart (user IS NULL) in the store by ID with no authorize!(..., carttoken). Contrast CartResolvable#findcart!, which binds the token.
ruby spree/api/app/controllers/spree/api/v3/store/cartscontroller.rb:177-178 def findcartforassociation currentstore.carts.where(user: [nil, currentuser]).findbyprefixid!(params[:id]) end
Identifier. prefixedid is "cart" + SQIDS.encode([id]) with SQIDS = Sqids.new(minlength: 10) (prefixedid.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.
ruby spree/core/app/models/concerns/spree/prefixedid.rb:17-56 SQIDS = Sqids.new(minlength: 10)
def prefixedid return nil unless id.present?
"#{self.class.prefixidprefix}#{Spree::PrefixedId::SQIDS.encode([id])}" end
Data flow. Spree.cartassociateservice.call(guestorder: @cart, user: currentuser, guestonly: true) reassigns the owner and overwrites email, preserving existing addresses via billaddress ||= / shipaddress ||=. rendercart then serializes billingaddress/shippingaddress (firstname, lastname, address1, address2, city, postalcode, 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 loginrequired mode.
1. Authenticate: POST /api/v3/store/auth/login → attacker JWT. 2. Derive candidate IDs offline: "cart" + Sqids.encode([n]) for a range of n. 3. For each candidate: PATCH /api/v3/store/carts/<id>/associate with the attacker JWT. A hit returns 200 with the victim's billingaddress/shippingaddress; non-guest or missing carts return 404/422.
Impact
Confidentiality: an authenticated attacker can enumerate guest cart IDs and read checkout PII (name, street, postal code, phone) on carts they don't own. Integrity: limited and recoverable — each call reassigns the guest cart and overwrites its email, disrupting the original guest's in-progress cart. Requires a registered account, so not anonymously exploitable.
Remediation
Update to Spree 5.4.4 or 5.5.4. Your storefront, based on https://github.com/spree/storefront, doesn't need any updates because it has always sent a cart token when associating carts; this is a backend issue.