GHSA-4825-p4xm-pcf2: High severity rubygems/spree_api vulnerability
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.
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
rubygems/spree_apito a version that resolves this vulnerability.Fixed in 5.5.4 - Upgrade
Upgrade
rubygems/spree_apito a version that resolves this vulnerability.Fixed in 5.4.4 - Upgrade
Upgrade
Spreeto a version that resolves this vulnerability.Fixed in 5.4.4 - Upgrade
Upgrade
Spreeto a version that resolves this vulnerability.Fixed in 5.5.4
Event History
Frequently Asked Questions
Which stores and carts are exposed?
Exposure requires a store that is not running in login_required mode. Target carts must be guest carts that are not yet associated with a user and that already contain checkout address information.
What access does an attacker need?
An attacker needs a low-privilege authenticated customer account. They can target guest carts by supplying cart IDs, which are reversible encodings of auto-incrementing primary keys rather than possession tokens.
What can an attacker obtain or change?
An attacker can associate another user's eligible guest cart with their own account and read checkout addresses stored on that cart. They can also cause recoverable integrity changes, including cart reassignment and overwriting the email on an in-progress cart.