CVE-2024-31205: Insufficient Information (NVD-CWE-noinfo)
value: Saleor is an e-commerce platform. Starting in version 3.10.0 and prior to versions 3.14.64, 3.15.39, 3.16.39, 3.17.35, 3.18.31, and 3.19.19, an attacker may bypass cross-set request forgery (CSRF) validation when calling refresh token mutation with empty string. When a user provides an empty string in `refreshToken` mutation, while the token persists in `JWT_REFRESH_TOKEN_COOKIE_NAME` cookie, application omits validation against CSRF token and returns valid access token. Versions 3.14.64, 3.15.39, 3.16.39, 3.17.35, 3.18.31, and 3.19.19 contain a patch for the issue. As a workaround, one may replace `saleor.graphql.account.mutations.authentication.refresh_token.py.get_refresh_token`. This will fix the issue, but be aware, that it returns `JWT_MISSING_TOKEN` instead of `JWT_INVALID_TOKEN`.

CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-04-08T15:15Z
Repository: https://github.com/saleor/saleor
Language: Python
Files changed: saleor/graphql/account/mutations/authentication/refresh_token.py, saleor/graphql/account/tests/mutations/authentication/test_token_refresh.py
Commit date: 2024-04-02 13:13:36 +0200
Commit message: Advisory fix merx-280.

Diff: diff --git a/saleor/graphql/account/mutations/authentication/refresh_token.py b/saleor/graphql/account/mutations/authentication/refresh_token.py
index cf4fb63484b..b461fffc36d 100644
--- a/saleor/graphql/account/mutations/authentication/refresh_token.py
+++ b/saleor/graphql/account/mutations/authentication/refresh_token.py
@@ -56,18 +56,17 @@ class RefreshToken(BaseMutation):
     @classmethod
     def get_refresh_token(
         cls, info: ResolveInfo, refresh_token: Optional[str] = None
     ) -> Optional[str]:
         request = info.context
-        refresh_token = refresh_token or request.COOKIES.get(
-            JWT_REFRESH_TOKEN_COOKIE_NAME, None
-        )
+        if refresh_token is None:
+            refresh_token = request.COOKIES.get(JWT_REFRESH_TOKEN_COOKIE_NAME, None)
         return refresh_token
 
     @classmethod
     def clean_refresh_token(cls, refresh_token):
-        if not refresh_token:
+        if refresh_token is None:
             raise ValidationError(
                 {
                     "refresh_token": ValidationError(
                         "Missing refreshToken",
                         code=AccountErrorCode.JWT_MISSING_TOKEN.value,
diff --git a/saleor/graphql/account/tests/mutations/authentication/test_token_refresh.py b/saleor/graphql/account/tests/mutations/authentication/test_token_refresh.py
index e969dbcbf49..f0baffa79d4 100644
--- a/saleor/graphql/account/tests/mutations/authentication/test_token_refresh.py
+++ b/saleor/graphql/account/tests/mutations/authentication/test_token_refresh.py
@@ -1,7 +1,8 @@
 from datetime import datetime
 
+import pytest
 from django.urls import reverse
 from freezegun import freeze_time
 
 from ......account.error_codes import AccountErrorCode
 from ......core.jwt import (
@@ -267,5 +268,27 @@ def test_refresh_token_when_user_deactivated_token(api_client, customer_user):
     errors = data["errors"]
 
     assert not data["token"]
     assert len(errors) == 1
     assert errors[0]["code"] == AccountErrorCode.JWT_INVALID_TOKEN.name
+
+
+@pytest.mark.parametrize("token", ["incorrect-token", ""])
+def test_refresh_token_incorrect_token_provided(api_client, customer_user, token):
+    # given
+    csrf_token = _get_new_csrf_token()
+    refresh_token = create_refresh_token(customer_user, {"csrfToken": csrf_token})
+    api_client.cookies[JWT_REFRESH_TOKEN_COOKIE_NAME] = refresh_token
+    api_client.cookies[JWT_REFRESH_TOKEN_COOKIE_NAME]["httponly"] = True
+
+    variables = {"token": token, "csrf_token": csrf_token}
+
+    # when
+    response = api_client.post_graphql(MUTATION_TOKEN_REFRESH, variables)
+    content = get_graphql_content(response)
+
+    # then
+    data = content["data"]["tokenRefresh"]
+    errors = data["errors"]
+    assert not data.get("token")
+    assert len(errors) == 1
+    assert errors[0]["code"] == AccountErrorCode.JWT_DECODE_ERROR.name

Vulnerable code:         refresh_token = refresh_token or request.COOKIES.get(
            JWT_REFRESH_TOKEN_COOKIE_NAME, None
        )
        if not refresh_token:
Fixed code:         if refresh_token is None:
            refresh_token = request.COOKIES.get(JWT_REFRESH_TOKEN_COOKIE_NAME, None)
        if refresh_token is None:
import pytest


@pytest.mark.parametrize("token", ["incorrect-token", ""])
def test_refresh_token_incorrect_token_provided(api_client, customer_user, token):
    # given
    csrf_token = _get_new_csrf_token()
    refresh_token = create_refresh_token(customer_user, {"csrfToken": csrf_token})
    api_client.cookies[JWT_REFRESH_TOKEN_COOKIE_NAME] = refresh_token
    api_client.cookies[JWT_REFRESH_TOKEN_COOKIE_NAME]["httponly"] = True

    variables = {"token": token, "csrf_token": csrf_token}

    # when
    response = api_client.post_graphql(MUTATION_TOKEN_REFRESH, variables)
    content = get_graphql_content(response)

    # then
    data = content["data"]["tokenRefresh"]
    errors = data["errors"]
    assert not data.get("token")
    assert len(errors) == 1
    assert errors[0]["code"] == AccountErrorCode.JWT_DECODE_ERROR.name