CVE-2024-2032 (published 2024-06-06T19:15Z)

A race condition vulnerability exists in zenml-io/zenml versions up to and including 0.55.3, which allows for the creation of multiple users with the same username when requests are sent in parallel. This issue was fixed in version 0.55.5. The vulnerability arises due to insufficient handling of concurrent user creation requests, leading to data inconsistencies and potential authentication problems. Specifically, concurrent processes may overwrite or corrupt user data, complicating user identification and posing security risks. This issue is particularly concerning for APIs that rely on usernames as input parameters, such as PUT /api/v1/users/test_race, where it could lead to further complications.


Weakness information:
cwe_id: CWE-366
cwe_name: Race Condition within a Thread
description: If two threads of execution use a resource simultaneously, there exists the possibility that resources may be used while invalid, in turn making the state of execution undefined.
extended_description: 
            
             
			 
         
         

Fix information:
repo_url: https://github.com/zenml-io/zenml
commit_details: repo_url: https://github.com/zenml-io/zenml
author: Andrei Vishniakov
author_date: 2024-03-04 10:05:43+01:00
msg: Uniquely constrained users table (#2483)

* use db constrains for users

* lint a bit

* coderabbitai

* set rate limits for login api

* Revert "set rate limits for login api"

This reverts commit 78ce40e738e73ce7d9398c73559f3d29a502c24f.

* bring checks back

* Auto-update of Starter template

* Auto-update of NLP template

* review suggestions

* resolve branching

* Auto-update of Starter template

* Auto-update of NLP template

* lint

---------

Co-authored-by: GitHub Actions <actions@github.com>
repository: repo_url: https://github.com/zenml-io/zenml
repo_name: zenml-io/zenml
description: ZenML 🙏: Build portable, production-ready MLOps pipelines. https://zenml.io.
repo_language: Python
owner: zenml-io
file_changes: filename: 72675226b2de_unique_users.py
change_type: ModificationType.ADD
diff: @@ -0,0 +1,31 @@
+"""unique users [72675226b2de].
+
+Revision ID: 72675226b2de
+Revises: 0.55.4
+Create Date: 2024-02-29 14:58:25.584731
+
+"""
+
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision = "72675226b2de"
+down_revision = "0.55.4"
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+    """Upgrade database schema and/or data, creating a new revision."""
+    with op.batch_alter_table("user", schema=None) as batch_op:
+        batch_op.create_unique_constraint(
+            "uq_user_name_is_service_account", ["name", "is_service_account"]
+        )
+
+
+def downgrade() -> None:
+    """Downgrade database schema and/or data back to the previous revision."""
+    with op.batch_alter_table("user", schema=None) as batch_op:
+        batch_op.drop_constraint(
+            "uq_user_name_is_service_account", type_="unique"
+        )

programming_language: Python
filename: user_schemas.py
change_type: ModificationType.MODIFY
diff: @@ -17,7 +17,7 @@ from datetime import datetime
 from typing import TYPE_CHECKING, Any, List, Optional
 from uuid import UUID
 
-from sqlalchemy import TEXT, Column
+from sqlalchemy import TEXT, Column, UniqueConstraint
 from sqlmodel import Field, Relationship
 
 from zenml.models import (
@@ -65,6 +65,7 @@ class UserSchema(NamedSchema, table=True):
     """SQL Model for users."""
 
     __tablename__ = "user"
+    __table_args__ = (UniqueConstraint("name", "is_service_account"),)
 
     is_service_account: bool = Field(default=False)
     full_name: str

programming_language: Python
filename: sql_zen_store.py
change_type: ModificationType.MODIFY
diff: @@ -5146,28 +5146,29 @@ class SqlZenStore(BaseZenStore):
                 already exists.
         """
         with Session(self.engine) as session:
+            # Check if a service account with the given name already
+            # exists
+            err_msg = (
+                f"Unable to create service account with name "
+                f"'{service_account.name}': Found existing service "
+                "account with this name."
+            )
+            try:
+                self._get_account_schema(
+                    service_account.name, session=session, service_account=True
+                )
+                raise EntityExistsError(err_msg)
+            except KeyError:
+                pass
+
             # Create the service account
             new_account = UserSchema.from_service_account_request(
                 service_account
             )
             session.add(new_account)
+            # on commit an IntegrityError may arise we let it bubble up
+            session.commit()
 
-            # Check if a service account with the given name already
-            # exists
-            service_accounts = session.execute(
-                select(UserSchema).where(
-                    UserSchema.name == service_account.name,
-                    UserSchema.is_service_account.is_(True),  # type: ignore[attr-defined]
-                )
-            ).fetchall()
-            if len(service_accounts) == 1:
-                session.commit()
-            else:
-                raise EntityExistsError(
-                    f"Unable to create service account with name "
-                    f"'{service_account.name}': Found existing service "
-                    "account with this name."
-                )
             return new_account.to_service_account_model(include_metadata=True)
 
     def get_service_account(
@@ -7357,24 +7358,27 @@ class SqlZenStore(BaseZenStore):
                 already exists.
         """
         with Session(self.engine) as session:
+            # Check if a user account with the given name already exists
+            err_msg = (
+                f"Unable to create user with name '{user.name}': "
+                f"Found an existing user account with this name."
+            )
+            try:
+                self._get_account_schema(
+                    user.name,
+                    session=session,
+                    # Filter out service accounts
+                    service_account=False,
+                )
+                raise EntityExistsError(err_msg)
+            except KeyError:
+                pass
+
             # Create the user
             new_user = UserSchema.from_user_request(user)
             session.add(new_user)
-
-            # Check if a user account with the given name already exists
-            users = session.execute(
-                select(UserSchema).where(
-                    UserSchema.name == user.name,
-                    UserSchema.is_service_account.is_(False),  # type: ignore[attr-defined]
-                )
-            ).fetchall()
-            if len(users) == 1:
-                session.commit()
-            else:
-                raise EntityExistsError(
-                    f"Unable to create user with name '{user.name}': "
-                    f"Found an existing user account with this name."
-                )
+            # on commit an IntegrityError may arise we let it bubble up
+            session.commit()
             return new_user.to_model(include_metadata=True)
 
     def get_user(

programming_language: Python
filename: test_zen_store.py
change_type: ModificationType.MODIFY
diff: @@ -22,6 +22,7 @@ from uuid import UUID, uuid4
 
 import pytest
 from pydantic import SecretStr
+from sqlalchemy.exc import IntegrityError
 
 from tests.integration.functional.utils import sample_name
 from tests.integration.functional.zen_stores.utils import (
@@ -422,7 +423,7 @@ def test_creating_users_in_parallel_do_not_duplicate_fails(
         """
         try:
             clean_client.zen_store.create_user(user_request)
-        except EntityExistsError:
+        except (EntityExistsError, IntegrityError):
             pass
 
     user_name = "test_user"
@@ -463,7 +464,7 @@ def test_creating_service_accounts_in_parallel_do_not_duplicate_fails(
             clean_client.zen_store.create_service_account(
                 service_account_request
             )
-        except EntityExistsError:
+        except (EntityExistsError, IntegrityError):
             pass
 
     user_name = "test_user"

programming_language: Python