VYPR
Moderate severityNVD Advisory· Published Jan 24, 2020· Updated Aug 4, 2024

CVE-2020-7964

CVE-2020-7964

Description

Mirumee Saleor 2.x before 2.9.1 has incorrect access control in checkoutCustomerAttach mutations, allowing attackers to attach checkouts to any user ID and leak user data.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Mirumee Saleor 2.x before 2.9.1 has incorrect access control in checkoutCustomerAttach mutations, allowing attackers to attach checkouts to any user ID and leak user data.

Vulnerability

Description

CVE-2020-7964 is an incorrect access control vulnerability in Mirumee Saleor versions 2.x prior to 2.9.1. The checkoutCustomerAttach mutation failed to verify that the provided customer ID matches the currently authenticated user [1][2]. This allowed any authenticated user to attach a checkout session to any other user's ID by simply specifying an arbitrary user identifier in the mutation request [3].

Exploitation

An attacker could exploit this by first creating a checkout session and then calling the checkoutCustomerAttach mutation with a target user ID. Since user IDs are integers and enumerable, an attacker could brute force valid IDs with sufficient effort [3]. The mutation then returns the modified checkout object, which contains user information [3].

Impact

Successful exploitation leads to unauthorized disclosure of sensitive user data. The attacker can retrieve personally identifiable information such as first and last name, address book contents, order history, and stored payment method details (card type, last four digits, expiration date) of other users [1][3]. This vulnerability impacts confidentiality and potentially privacy compliance.

Mitigation

The issue was fixed in Saleor version 2.9.1, which was released on the same day as the vulnerability disclosure [1][3]. The fix introduces proper permission checks in the mutation and also restricts access to the user field in several other objects (Checkout.events, Checkout.user, CustomerEvent.user, GiftCard.user, Order.events, Order.user, OrderEvent.user, User.storedPaymentSources) to ensure that only the owning user or a privileged admin can read them [2][3].

AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
saleorPyPI
>= 2.0.0, < 2.9.12.9.1

Affected products

2
  • Mirumee/Saleordescription
  • ghsa-coords
    Range: >= 2.0.0, < 2.9.1

Patches

1
233b8890c60f

Merge pull request #5192 from maarcingebala/fix-checkout-attach-user-permissions

https://github.com/mirumee/saleorMarcin GębalaJan 23, 2020via ghsa
4 files changed · +64 13
  • CHANGELOG.md+1 0 modified
    @@ -54,6 +54,7 @@ All notable, unreleased changes to this project will be documented in this file.
     - Drop gettext occurrences - #5189 by @IKarbowiak
     - Fix `product_created` webhook - #5187 by @dzkb
     - Drop unused resolver `resolve_availability` - #5190 by @maarcingebala
    +- Fix permission for `checkoutCustomerAttach` mutation - #5192 by @maarcingebala
     
     ## 2.9.0
     
    
  • saleor/graphql/checkout/mutations.py+34 7 modified
    @@ -5,6 +5,7 @@
     from django.core.exceptions import ObjectDoesNotExist, ValidationError
     from django.db import transaction
     from django.db.models import Prefetch
    +from graphql_jwt.exceptions import PermissionDenied
     
     from ...account.error_codes import AccountErrorCode
     from ...checkout import models
    @@ -35,7 +36,7 @@
     from ...product import models as product_models
     from ...warehouse.availability import check_stock_quantity, get_available_quantity
     from ..account.i18n import I18nMixin
    -from ..account.types import AddressInput, User
    +from ..account.types import AddressInput
     from ..core.mutations import (
         BaseMutation,
         ClearMetaBaseMutation,
    @@ -404,22 +405,39 @@ class CheckoutCustomerAttach(BaseMutation):
     
         class Arguments:
             checkout_id = graphene.ID(required=True, description="ID of the checkout.")
    -        customer_id = graphene.ID(required=True, description="The ID of the customer.")
    +        customer_id = graphene.ID(
    +            required=False,
    +            description=(
    +                "The ID of the customer. DEPRECATED: This field is deprecated and will "
    +                "be removed in Saleor 2.11. To identify a customer you should "
    +                "authenticate with JWT token."
    +            ),
    +        )
     
         class Meta:
             description = "Sets the customer as the owner of the checkout."
             error_type_class = CheckoutError
             error_type_field = "checkout_errors"
     
         @classmethod
    -    def perform_mutation(cls, _root, info, checkout_id, customer_id):
    +    def check_permissions(cls, context):
    +        return context.user.is_authenticated
    +
    +    @classmethod
    +    def perform_mutation(cls, _root, info, checkout_id, customer_id=None):
             checkout = cls.get_node_or_error(
                 info, checkout_id, only_type=Checkout, field="checkout_id"
             )
    -        customer = cls.get_node_or_error(
    -            info, customer_id, only_type=User, field="customer_id"
    -        )
    -        checkout.user = customer
    +
    +        # Check if provided customer_id matches with the authenticated user and raise
    +        # error if it doesn't. This part can be removed when `customer_id` field is
    +        # removed.
    +        if customer_id:
    +            current_user_id = graphene.Node.to_global_id("User", info.context.user.id)
    +            if current_user_id != customer_id:
    +                raise PermissionDenied()
    +
    +        checkout.user = info.context.user
             checkout.save(update_fields=["user", "last_change"])
             return CheckoutCustomerAttach(checkout=checkout)
     
    @@ -435,11 +453,20 @@ class Meta:
             error_type_class = CheckoutError
             error_type_field = "checkout_errors"
     
    +    @classmethod
    +    def check_permissions(cls, context):
    +        return context.user.is_authenticated
    +
         @classmethod
         def perform_mutation(cls, _root, info, checkout_id):
             checkout = cls.get_node_or_error(
                 info, checkout_id, only_type=Checkout, field="checkout_id"
             )
    +
    +        # Raise error if the current user doesn't own the checkout of the given ID.
    +        if checkout.user and checkout.user != info.context.user:
    +            raise PermissionDenied()
    +
             checkout.user = None
             checkout.save(update_fields=["user", "last_change"])
             return CheckoutCustomerDetach(checkout=checkout)
    
  • saleor/graphql/schema.graphql+1 1 modified
    @@ -2288,7 +2288,7 @@ type Mutation {
       checkoutBillingAddressUpdate(billingAddress: AddressInput!, checkoutId: ID!): CheckoutBillingAddressUpdate
       checkoutComplete(checkoutId: ID!, redirectUrl: String, storeSource: Boolean = false): CheckoutComplete
       checkoutCreate(input: CheckoutCreateInput!): CheckoutCreate
    -  checkoutCustomerAttach(checkoutId: ID!, customerId: ID!): CheckoutCustomerAttach
    +  checkoutCustomerAttach(checkoutId: ID!, customerId: ID): CheckoutCustomerAttach
       checkoutCustomerDetach(checkoutId: ID!): CheckoutCustomerDetach
       checkoutEmailUpdate(checkoutId: ID, email: String!): CheckoutEmailUpdate
       checkoutLineDelete(checkoutId: ID!, lineId: ID): CheckoutLineDelete
    
  • tests/api/test_checkout.py+28 5 modified
    @@ -8,6 +8,7 @@
     from django.core.exceptions import ValidationError
     from prices import Money, TaxedMoney
     
    +from saleor.account.models import User
     from saleor.checkout import calculations
     from saleor.checkout.error_codes import CheckoutErrorCode
     from saleor.checkout.models import Checkout
    @@ -25,7 +26,7 @@
     from saleor.shipping import ShippingMethodType
     from saleor.shipping.models import ShippingMethod
     from saleor.warehouse.models import Stock
    -from tests.api.utils import get_graphql_content
    +from tests.api.utils import assert_no_permission, get_graphql_content
     
     
     @pytest.fixture
    @@ -851,7 +852,9 @@ def test_checkout_line_delete_by_zero_quantity(
         mocked_update_shipping_method.assert_called_once_with(checkout, mock.ANY)
     
     
    -def test_checkout_customer_attach(user_api_client, checkout_with_item, customer_user):
    +def test_checkout_customer_attach(
    +    api_client, user_api_client, checkout_with_item, customer_user
    +):
         checkout = checkout_with_item
         assert checkout.user is None
     
    @@ -871,16 +874,26 @@ def test_checkout_customer_attach(user_api_client, checkout_with_item, customer_
         """
         checkout_id = graphene.Node.to_global_id("Checkout", checkout.pk)
         customer_id = graphene.Node.to_global_id("User", customer_user.pk)
    -
         variables = {"checkoutId": checkout_id, "customerId": customer_id}
    +
    +    # Mutation should fail for unauthenticated customers
    +    response = api_client.post_graphql(query, variables)
    +    assert_no_permission(response)
    +
    +    # Mutation should succeed for authenticated customer
         response = user_api_client.post_graphql(query, variables)
         content = get_graphql_content(response)
    -
         data = content["data"]["checkoutCustomerAttach"]
         assert not data["errors"]
         checkout.refresh_from_db()
         assert checkout.user == customer_user
     
    +    # Mutation with ID of a different user should fail as well
    +    other_customer = User.objects.create_user("othercustomer@example.com", "password")
    +    variables["customerId"] = graphene.Node.to_global_id("User", other_customer.pk)
    +    response = user_api_client.post_graphql(query, variables)
    +    assert_no_permission(response)
    +
     
     MUTATION_CHECKOUT_CUSTOMER_DETACH = """
         mutation checkoutCustomerDetach($checkoutId: ID!) {
    @@ -904,16 +917,26 @@ def test_checkout_customer_detach(user_api_client, checkout_with_item, customer_
     
         checkout_id = graphene.Node.to_global_id("Checkout", checkout.pk)
         variables = {"checkoutId": checkout_id}
    +
    +    # Mutation should succeed if the user owns this checkout.
         response = user_api_client.post_graphql(
             MUTATION_CHECKOUT_CUSTOMER_DETACH, variables
         )
         content = get_graphql_content(response)
    -
         data = content["data"]["checkoutCustomerDetach"]
         assert not data["errors"]
         checkout.refresh_from_db()
         assert checkout.user is None
     
    +    # Mutation should fail when user calling it doesn't own the checkout.
    +    other_user = User.objects.create_user("othercustomer@example.com", "password")
    +    checkout.user = other_user
    +    checkout.save()
    +    response = user_api_client.post_graphql(
    +        MUTATION_CHECKOUT_CUSTOMER_DETACH, variables
    +    )
    +    assert_no_permission(response)
    +
     
     MUTATION_CHECKOUT_SHIPPING_ADDRESS_UPDATE = """
         mutation checkoutShippingAddressUpdate(
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.