CVE-2024-29888 (Python, published 2024-03-27T19:15Z)
Description: value: Saleor is an e-commerce platform that serves high-volume companies. When using `Pickup: Local stock only` click-and-collect as a delivery method in specific conditions the customer could overwrite the warehouse address with its own, which exposes its address as click-and-collect address. This issue has been patched in versions: `3.14.61`, `3.15.37`, `3.16.34`, `3.17.32`, `3.18.28`, `3.19.15`.
Severity: nan (CVSS v3 base score: )
Weakness: NVD-CWE-noinfo \u2014 Insufficient Information
There is insufficient information about the issue to classify it; details are unkown or unspecified.
Repository: https://github.com/saleor/saleor
Files changed:
saleor/checkout/error_codes.py
saleor/graphql/checkout/mutations/checkout_delivery_method_update.py
saleor/graphql/checkout/mutations/checkout_shipping_address_update.py
saleor/graphql/checkout/mutations/utils.py
saleor/graphql/checkout/tests/mutations/test_checkout_delivery_method_update.py
saleor/graphql/checkout/tests/mutations/test_checkout_shipping_address_update.py
saleor/graphql/schema.graphql
Commit message: Fix  tax calculation for Click and Collect option. (#15505)



Vulnerable code:
            "of the checkout." + ADDED_IN_31
            update_fields=[
                "shipping_method",
                "collection_point",
            ]
            + invalidate_prices_updated_fields

@pytest.mark.parametrize("is_valid_delivery_method", [True, False])
def test_checkout_delivery_method_update_with_not_all_required_shipping_address_data(
    is_valid_delivery_method,
    mock_clean_delivery.return_value = is_valid_delivery_method
    mock_clean_delivery.return_value = is_valid_delivery_method
    if is_valid_delivery_method:
        assert not errors
        assert getattr(checkout, attribute_name) == delivery_method
    else:
        assert len(errors) == 1
        assert errors[0]["field"] == "deliveryMethodId"
        assert (
            errors[0]["code"] == CheckoutErrorCode.DELIVERY_METHOD_NOT_APPLICABLE.name
        )
        assert checkout.shipping_method is None
        assert checkout.collection_point is None
@pytest.mark.parametrize("is_valid_delivery_method", [True, False])
    ("delivery_method", "node_name", "attribute_name"),
def test_checkout_delivery_method_update_with_not_valid_address_data(
    is_valid_delivery_method,
    mock_clean_delivery.return_value = is_valid_delivery_method
    mock_clean_delivery.return_value = is_valid_delivery_method
    if is_valid_delivery_method:
        assert not errors
        assert getattr(checkout, attribute_name) == delivery_method
    else:
        assert len(errors) == 1
        assert errors[0]["field"] == "deliveryMethodId"
        assert (
            errors[0]["code"] == CheckoutErrorCode.DELIVERY_METHOD_NOT_APPLICABLE.name
        )
        assert checkout.shipping_method is None
        assert checkout.collection_point is None
  Updates the delivery method (shipping method or pick up point) of the checkout.
Updates the delivery method (shipping method or pick up point) of the checkout.

Fixed code:
    SHIPPING_CHANGE_FORBIDDEN = "shipping_change_forbidden"
from ....warehouse import WarehouseClickAndCollectOption
            "of the checkout. "
            "Updates the checkout shipping_address for click and collect delivery "
            "for a warehouse address. " + ADDED_IN_31
        checkout_fields_to_update = ["shipping_method", "collection_point"]
        if (
            collection_point is not None
            and collection_point.click_and_collect_option
            == WarehouseClickAndCollectOption.LOCAL_STOCK
        ):
            checkout.shipping_address = collection_point.address
            checkout_info.shipping_address = collection_point.address
            checkout_fields_to_update += ["shipping_address"]
            update_fields=checkout_fields_to_update + invalidate_prices_updated_fields
    ERROR_CC_ADDRESS_CHANGE_FORBIDDEN,
        # prevent from changing the shipping address when click and collect is used.
        if checkout.collection_point_id:
            raise ValidationError(
                {
                    "shipping_address": ValidationError(
                        ERROR_CC_ADDRESS_CHANGE_FORBIDDEN,
                        code=CheckoutErrorCode.SHIPPING_CHANGE_FORBIDDEN.value,
                    )
                }
            )
ERROR_CC_ADDRESS_CHANGE_FORBIDDEN = (
    "Can't change shipping address manually. "
    "For click and collect delivery, address is set to a warehouse address."
)
@pytest.mark.parametrize(
    ("delivery_method", "node_name", "attribute_name"),
    [
        ("shipping_method", "ShippingMethod", "shipping_method"),
    ],
    indirect=("delivery_method",),
)
@patch(
    "saleor.graphql.checkout.mutations.checkout_delivery_method_update."
    "clean_delivery_method"
)
def test_checkout_delivery_method_update_valid_method_not_all_shipping_data(
    mock_clean_delivery,
    api_client,
    delivery_method,
    node_name,
    attribute_name,
    checkout_with_item_for_cc,
):
    # given
    mock_clean_delivery.return_value = True

    checkout = checkout_with_item_for_cc
    checkout.shipping_address = Address.objects.create(country="US")
    checkout.save()
    manager = get_plugins_manager(allow_replica=False)
    lines, _ = fetch_checkout_lines(checkout)
    checkout_info = fetch_checkout_info(checkout, lines, manager)
    shipping_method_data = convert_to_shipping_method_data(
        delivery_method,
        delivery_method.channel_listings.get(),
    )
    query = MUTATION_UPDATE_DELIVERY_METHOD
    mock_clean_delivery.return_value = True

    method_id = graphene.Node.to_global_id(node_name, delivery_method.id)

    # when
    response = api_client.post_graphql(
        query, {"id": to_global_id_or_none(checkout), "deliveryMethodId": method_id}
    )

    # then
    data = get_graphql_content(response)["data"]["checkoutDeliveryMethodUpdate"]
    checkout.refresh_from_db()

    mock_clean_delivery.assert_called_once_with(
        checkout_info=checkout_info, lines=lines, method=shipping_method_data
    )
    errors = data["errors"]

    assert not errors
    assert getattr(checkout, attribute_name) == delivery_method


@pytest.mark.parametrize(
    "delivery_method, node_name, attribute_name",  # noqa: PT006
    [
        ("warehouse", "Warehouse", "collection_point"),
    ],
    indirect=("delivery_method",),
)
@patch(
    "saleor.graphql.checkout.mutations.checkout_delivery_method_update."
    "clean_delivery_method"
)
def test_checkout_delivery_method_update_valid_method_not_all_shipping_data_for_cc(
    mock_clean_delivery,
    api_client,
    delivery_method,
    node_name,
    attribute_name,
    checkout_with_item_for_cc,
    warehouse_for_cc,
):
    # given
    mock_clean_delivery.return_value = True
    checkout_address = Address.objects.create(country="US")
    checkout = checkout_with_item_for_cc
    checkout.shipping_address = checkout_address
    checkout.save()
    manager = get_plugins_manager(allow_replica=False)
    lines, _ = fetch_checkout_lines(checkout)
    checkout_info = fetch_checkout_info(checkout, lines, manager)

    shipping_method_data = delivery_method
    checkout_info.shipping_address = shipping_method_data.address
    query = MUTATION_UPDATE_DELIVERY_METHOD
    mock_clean_delivery.return_value = True

    method_id = graphene.Node.to_global_id(node_name, delivery_method.id)

    # when
    response = api_client.post_graphql(
        query, {"id": to_global_id_or_none(checkout), "deliveryMethodId": method_id}
    )

    # then
    data = get_graphql_content(response)["data"]["checkoutDeliveryMethodUpdate"]
    checkout.refresh_from_db()

    mock_clean_delivery.assert_called_once_with(
        checkout_info=checkout_info, lines=lines, method=shipping_method_data
    )
    errors = data["errors"]
    assert checkout.shipping_address == delivery_method.address
    assert not errors
    assert getattr(checkout, attribute_name) == delivery_method


def test_checkout_delivery_method_update_invalid_method_not_all_shipping_data(
    mock_clean_delivery.return_value = False
    mock_clean_delivery.return_value = False

    assert len(errors) == 1
    assert errors[0]["field"] == "deliveryMethodId"
    assert errors[0]["code"] == CheckoutErrorCode.DELIVERY_METHOD_NOT_APPLICABLE.name
    assert checkout.shipping_method is None
    assert checkout.collection_point is None
    "delivery_method, node_name, attribute_name",  # noqa: PT006
def test_checkout_delivery_method_update_invalid_with_not_valid_address_data(
    mock_clean_delivery.return_value = False
    mock_clean_delivery.return_value = False

    assert len(errors) == 1
    assert errors[0]["field"] == "deliveryMethodId"
    assert errors[0]["code"] == CheckoutErrorCode.DELIVERY_METHOD_NOT_APPLICABLE.name
    assert checkout.shipping_method is None
    assert checkout.collection_point is None


@pytest.mark.parametrize(
    "delivery_method, node_name, attribute_name",  # noqa: PT006
    [
        ("shipping_method", "ShippingMethod", "shipping_method"),
    ],
    indirect=("delivery_method",),
)
@patch(
    "saleor.graphql.checkout.mutations.checkout_delivery_method_update."
    "clean_delivery_method"
)
def test_checkout_delivery_method_update_valid_with_not_valid_address_data(
    mock_clean_delivery,
    api_client,
    delivery_method,
    node_name,
    attribute_name,
    checkout_with_item_for_cc,
):
    # given
    mock_clean_delivery.return_value = True

    checkout = checkout_with_item_for_cc
    checkout.shipping_address = Address.objects.create(
        country="US",
        city="New York",
        city_area="ABC",
        street_address_1="New street",
        postal_code="53-601",
    )
    checkout.save()
    manager = get_plugins_manager(allow_replica=False)
    lines, _ = fetch_checkout_lines(checkout)
    checkout_info = fetch_checkout_info(checkout, lines, manager)

    shipping_method_data = convert_to_shipping_method_data(
        delivery_method,
        delivery_method.channel_listings.get(),
    )
    query = MUTATION_UPDATE_DELIVERY_METHOD
    mock_clean_delivery.return_value = True

    method_id = graphene.Node.to_global_id(node_name, delivery_method.id)

    # when
    response = api_client.post_graphql(
        query, {"id": to_global_id_or_none(checkout), "deliveryMethodId": method_id}
    )

    # then
    data = get_graphql_content(response)["data"]["checkoutDeliveryMethodUpdate"]
    checkout.refresh_from_db()

    mock_clean_delivery.assert_called_once_with(
        checkout_info=checkout_info, lines=lines, method=shipping_method_data
    )
    errors = data["errors"]

    assert not errors
    assert getattr(checkout, attribute_name) == delivery_method


@pytest.mark.parametrize(
    "delivery_method, node_name, attribute_name",  # noqa: PT006
    [
        ("warehouse", "Warehouse", "collection_point"),
    ],
    indirect=("delivery_method",),
)
@patch(
    "saleor.graphql.checkout.mutations.checkout_delivery_method_update."
    "clean_delivery_method"
)
def test_checkout_delivery_method_update_valid_with_not_valid_address_data_for_cc(
    mock_clean_delivery,
    api_client,
    delivery_method,
    node_name,
    attribute_name,
    checkout_with_item_for_cc,
):
    # given
    mock_clean_delivery.return_value = True

    checkout = checkout_with_item_for_cc
    checkout.shipping_address = Address.objects.create(
        country="US",
        city="New York",
        city_area="ABC",
        street_address_1="New street",
        postal_code="53-601",
    )
    checkout.save()
    manager = get_plugins_manager(allow_replica=False)
    lines, _ = fetch_checkout_lines(checkout)
    checkout_info = fetch_checkout_info(checkout, lines, manager)

    shipping_method_data = delivery_method
    checkout_info.shipping_address = shipping_method_data.address
    query = MUTATION_UPDATE_DELIVERY_METHOD
    mock_clean_delivery.return_value = True

    method_id = graphene.Node.to_global_id(node_name, delivery_method.id)

    # when
    response = api_client.post_graphql(
        query, {"id": to_global_id_or_none(checkout), "deliveryMethodId": method_id}
    )

    # then
    data = get_graphql_content(response)["data"]["checkoutDeliveryMethodUpdate"]
    checkout.refresh_from_db()

    mock_clean_delivery.assert_called_once_with(
        checkout_info=checkout_info, lines=lines, method=shipping_method_data
    )
    errors = data["errors"]

    assert not errors
    assert getattr(checkout, attribute_name) == delivery_method


def test_checkout_shipping_address_update_with_collection_point_already_set(
    user_api_client,
    checkout_with_item,
    graphql_address_data,
    warehouse_for_cc,
):
    checkout = checkout_with_item
    checkout.collection_point_id = warehouse_for_cc.id
    checkout.save(update_fields=["collection_point_id"])

    shipping_address = graphql_address_data
    variables = {
        "id": to_global_id_or_none(checkout),
        "shippingAddress": shipping_address,
    }

    response = user_api_client.post_graphql(
        MUTATION_CHECKOUT_SHIPPING_ADDRESS_UPDATE, variables
    )
    content = get_graphql_content(response)
    data = content["data"]["checkoutShippingAddressUpdate"]
    errors = data["errors"]
    assert errors[0]["code"] == CheckoutErrorCode.SHIPPING_CHANGE_FORBIDDEN.name
    assert errors[0]["field"] == "shippingAddress"
  Updates the delivery method (shipping method or pick up point) of the checkout. Updates the checkout shipping_address for click and collect delivery for a warehouse address. 
  SHIPPING_CHANGE_FORBIDDEN
Updates the delivery method (shipping method or pick up point) of the checkout. Updates the checkout shipping_address for click and collect delivery for a warehouse address.