
Loading

Loading
We use strictly necessary cookies to run this site, and analytics cookies to understand how it's used. See our Privacy Policy for details.
A GitHub-reviewed security advisory, not necessarily under active exploitation the way our CISA KEV advisories are, but a real, disclosed vulnerability in an open-source dependency.
Affected packages
Vulnerable: >= 5.4.0, < 5.4.4
Patched in 5.4.4
Vulnerable: >= 5.5.0, < 5.5.4
Patched in 5.5.4
Details
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.
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.
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.
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.
POST /api/v3/store/auth/login → attacker JWT."cart_" + Sqids.encode([n]) for a range of n.PATCH /api/v3/store/carts/<id>/associate with the attacker JWT. A hit returns 200 with the victim's billing_address/shipping_address; non-guest or missing carts return 404/422.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.
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.
EPSS, exploit probability
Low0.38%
estimated chance of real-world exploitation in the next 30 days, higher than 32.6% of every CVE FIRST.org scores
Refreshed 11/7/2026, via FIRST.org's EPSS model, not CVSS, this measures likelihood of exploitation, not how severe it would be.
Talk to us about this
Find out whether this dependency (or others like it) is actually reachable in your codebase.
Get help patching, or building dependency-scanning into your pipeline going forward.
A secure code review or supply chain review finds this before an advisory does.