CVE-2024-31205 (published 2024-04-08T15:15Z)

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`.\n


Weakness information:
cwe_id: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/saleor/saleor
commit_details: repo_url: https://github.com/saleor/saleor
author: zedzior
author_date: 2024-04-02 13:13:36+02:00
msg: Advisory fix merx-280.
repository: repo_url: https://github.com/saleor/saleor
repo_name: saleor/saleor
description: Saleor Core: the high performance, composable, headless commerce API.
repo_language: Python
owner: saleor
file_changes: filename: refresh_token.py
change_type: ModificationType.MODIFY
diff: @@ -58,14 +58,13 @@ class RefreshToken(BaseMutation):
         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(

programming_language: Python
filename: test_token_refresh.py
change_type: ModificationType.MODIFY
diff: @@ -1,5 +1,6 @@
 from datetime import datetime
 
+import pytest
 from django.urls import reverse
 from freezegun import freeze_time
 
@@ -269,3 +270,25 @@ def test_refresh_token_when_user_deactivated_token(api_client, customer_user):
     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

programming_language: Python