VYPR
Low severityNVD Advisory· Published Nov 26, 2024· Updated Nov 26, 2024

Password Policy Bypass Vulnerability in Fides Webserver

CVE-2024-52008

Description

Fides is an open-source privacy engineering platform. The user invite acceptance API endpoint lacks server-side password policy enforcement, allowing users to set arbitrarily weak passwords by bypassing client-side validation. While the UI enforces password complexity requirements, direct API calls can circumvent these checks, enabling the creation of accounts with passwords as short as a single character. When an email messaging provider is enabled and a new user account is created in the system, an invite email containing a special link is sent to the new user's email address. This link directs the new user to a page where they can set their initial password. While the user interface implements password complexity checks, these validations are only performed client-side. The underlying /api/v1/user/accept-invite API endpoint does not implement the same password policy validations. This vulnerability allows an invited user to set an extremely weak password for their own account during the initial account setup process. Therefore that specific user's account can be compromised easily by an attacker guessing or brute forcing the password. The vulnerability has been patched in Fides version 2.50.0. Users are advised to upgrade to this version or later to secure their systems against this threat. There are no known workarounds for this vulnerability.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
ethyca-fidesPyPI
< 2.50.02.50.0

Affected products

1

Patches

1
ce664da46ab7

Merge commit from fork

https://github.com/ethyca/fidesAndres TorresNov 20, 2024via ghsa
4 files changed · +88 20
  • CHANGELOG.md+3 0 modified
    @@ -41,6 +41,9 @@ The types of changes are:
     ### Docs
     - Added docs for PrivacyNoticeRegion type [#5488](https://github.com/ethyca/fides/pull/5488)
     
    +### Security
    + - Password Policy is now Enforced in Accept Invite API [CVE-2024-52008](https://github.com/ethyca/fides/security/advisories/GHSA-v7vm-rhmg-8j2r)
    +
     ## [2.49.1](https://github.com/ethyca/fidesplus/compare/2.49.0...2.49.1)
     
     ### Added
    
  • src/fides/api/api/v1/endpoints/user_endpoints.py+2 2 modified
    @@ -173,7 +173,7 @@ def update_user_password(
                 status_code=HTTP_401_UNAUTHORIZED, detail="Incorrect password."
             )
     
    -    current_user.update_password(db=db, new_password=b64_str_to_str(data.new_password))
    +    current_user.update_password(db=db, new_password=data.new_password)
     
         logger.info("Updated user with id: '{}'.", current_user.id)
         return current_user
    @@ -202,7 +202,7 @@ def force_update_password(
                 detail=f"User with ID {user_id} does not exist.",
             )
     
    -    user.update_password(db=db, new_password=b64_str_to_str(data.new_password))
    +    user.update_password(db=db, new_password=data.new_password)
         logger.info("Updated user with id: '{}'.", user.id)
         return user
     
    
  • src/fides/api/schemas/user.py+32 7 modified
    @@ -10,6 +10,7 @@
     from fides.api.schemas.oauth import AccessToken
     
     
    +
     class PrivacyRequestReviewer(FidesSchema):
         """Data we can expose via the PrivacyRequest.reviewer relation"""
     
    @@ -40,19 +41,29 @@ def validate_username(cls, username: str) -> str:
         def validate_password(cls, password: str) -> str:
             """Add some password requirements"""
             decoded_password = decode_password(password)
    -
    -        if len(decoded_password) < 8:
    +        return UserCreate._validate_password(decoded_password)
    +
    +    @staticmethod
    +    def _validate_password(password: str) -> str:
    +        """
    +        Validate password requirements.
    +            Raises:
    +                ValueError: If password does not meet requirements
    +            Returns:
    +                str: password
    +        """
    +        if len(password) < 8:
                 raise ValueError("Password must have at least eight characters.")
    -        if re.search("[0-9]", decoded_password) is None:
    +        if re.search("[\d]", password) is None:
                 raise ValueError("Password must have at least one number.")
    -        if re.search("[A-Z]", decoded_password) is None:
    +        if re.search("[A-Z]", password) is None:
                 raise ValueError("Password must have at least one capital letter.")
    -        if re.search("[a-z]", decoded_password) is None:
    +        if re.search("[a-z]", password) is None:
                 raise ValueError("Password must have at least one lowercase letter.")
    -        if re.search(r"[\W_]", decoded_password) is None:
    +        if re.search(r"[\W_]", password) is None:
                 raise ValueError("Password must have at least one symbol.")
     
    -        return decoded_password
    +        return password
     
     
     class UserCreateResponse(FidesSchema):
    @@ -102,12 +113,26 @@ class UserPasswordReset(FidesSchema):
         old_password: str
         new_password: str
     
    +    @field_validator("new_password")
    +    @classmethod
    +    def validate_new_password(cls, password: str) -> str:
    +        """Add some password requirements"""
    +        decoded_password = decode_password(password)
    +        return UserCreate._validate_password(decoded_password)
    +
     
     class UserForcePasswordReset(FidesSchema):
         """Only a new password, for the case where the user does not remember their password"""
     
         new_password: str
     
    +    @field_validator("new_password")
    +    @classmethod
    +    def validate_new_password(cls, password: str) -> str:
    +        """Add some password requirements"""
    +        decoded_password = decode_password(password)
    +        return UserCreate._validate_password(decoded_password)
    +
     
     class UserUpdate(FidesSchema):
         """Data required to update a FidesUser"""
    
  • tests/ops/api/v1/endpoints/test_user_endpoints.py+51 11 modified
    @@ -852,7 +852,7 @@ def test_update_different_user_password(
             application_user,
         ) -> None:
             OLD_PASSWORD = "oldpassword"
    -        NEW_PASSWORD = "newpassword"
    +        NEW_PASSWORD = "Newpassword1!"
             application_user.update_password(db=db, new_password=OLD_PASSWORD)
     
             auth_header = generate_auth_header_for_user(user=application_user, scopes=[])
    @@ -874,15 +874,15 @@ def test_update_different_user_password(
             application_user = application_user.refresh_from_db(db=db)
             assert application_user.credentials_valid(password=OLD_PASSWORD)
     
    -    def test_update_user_password_invalid(
    +    def test_update_user_password_invalid_old_password(
             self,
             api_client,
             db,
             url_no_id,
             application_user,
         ) -> None:
             OLD_PASSWORD = "oldpassword"
    -        NEW_PASSWORD = "newpassword"
    +        NEW_PASSWORD = "Newpassword1!"
             application_user.update_password(db=db, new_password=OLD_PASSWORD)
     
             auth_header = generate_auth_header_for_user(user=application_user, scopes=[])
    @@ -909,7 +909,7 @@ def test_update_user_password(
             application_user,
         ) -> None:
             OLD_PASSWORD = "oldpassword"
    -        NEW_PASSWORD = "newpassword"
    +        NEW_PASSWORD = "Newpassword1!"
             application_user.update_password(db=db, new_password=OLD_PASSWORD)
             auth_header = generate_auth_header_for_user(user=application_user, scopes=[])
             resp = api_client.post(
    @@ -934,7 +934,7 @@ def test_force_update_different_user_password_without_scope(
             application_user,
         ) -> None:
             """A user without the proper scope cannot change another user's password"""
    -        NEW_PASSWORD = "newpassword"
    +        NEW_PASSWORD = "Newpassword1!"
             old_hashed_password = user.hashed_password
     
             auth_header = generate_auth_header_for_user(user=application_user, scopes=[])
    @@ -965,7 +965,7 @@ def test_force_update_different_user_password(
             A user with the right scope should be able to set a new password
             for another user.
             """
    -        NEW_PASSWORD = "newpassword"
    +        NEW_PASSWORD = "Newpassword1!"
             auth_header = generate_auth_header_for_user(
                 user=application_user, scopes=[USER_PASSWORD_RESET]
             )
    @@ -982,6 +982,46 @@ def test_force_update_different_user_password(
             user = user.refresh_from_db(db=db)
             assert user.credentials_valid(password=NEW_PASSWORD)
     
    +    @pytest.mark.parametrize(
    +        "new_password, expected_error",
    +        [
    +            ("short", "Value error, Password must have at least eight characters."),
    +            ("longerpassword", "Value error, Password must have at least one number."),
    +            ("longer55password", "Value error, Password must have at least one capital letter."),
    +            ("LONGER55PASSWORD", "Value error, Password must have at least one lowercase letter."),
    +            ("LoNgEr55paSSworD", "Value error, Password must have at least one symbol."),
    +        ],
    +    )
    +    def test_force_update_bad_password(
    +        self,
    +        api_client,
    +        db,
    +        url_no_id,
    +        user,
    +        application_user,
    +        new_password,
    +        expected_error,
    +    ) -> None:
    +        """
    +        A user with the right scope should be able to set a new password
    +        for another user.
    +        """
    +        auth_header = generate_auth_header_for_user(
    +            user=application_user, scopes=[USER_PASSWORD_RESET]
    +        )
    +
    +        resp = api_client.post(
    +            f"{url_no_id}/{user.id}/force-reset-password",
    +            headers=auth_header,
    +            json={
    +                "new_password": str_to_b64_str(new_password),
    +            },
    +        )
    +
    +        assert resp.status_code == HTTP_422_UNPROCESSABLE_ENTITY
    +        assert expected_error in resp.json()["detail"][0]["msg"]
    +        db.expunge(user)
    +
         def test_force_update_non_existent_user(
             self,
             api_client,
    @@ -991,7 +1031,7 @@ def test_force_update_non_existent_user(
             """
             Resetting on a user that does not exist should 404
             """
    -        NEW_PASSWORD = "newpassword"
    +        NEW_PASSWORD = "Newpassword1!"
             auth_header = generate_auth_header_for_user(
                 user=application_user, scopes=[USER_PASSWORD_RESET]
             )
    @@ -1902,7 +1942,7 @@ def test_accept_invite_valid(
             response = api_client.post(
                 url,
                 params={"username": "valid_user", "invite_code": "valid_code"},
    -            json={"username": "valid_user", "new_password": "pass"},
    +            json={"username": "valid_user", "new_password": "Testpassword1!"},
             )
     
             assert response.status_code == HTTP_200_OK
    @@ -1925,7 +1965,7 @@ def test_accept_invite_invalid_code(self, db, api_client, url):
             response = api_client.post(
                 url,
                 params={"username": "valid_user", "invite_code": "invalid_code"},
    -            json={"username": "valid_user", "new_password": "pass"},
    +            json={"username": "valid_user", "new_password": "Testpassword1!"},
             )
             assert response.status_code == HTTP_400_BAD_REQUEST
             assert response.json()["detail"] == "Invite code is invalid."
    @@ -1943,7 +1983,7 @@ def test_accept_invite_expired_code(self, mock_get_by, api_client: TestClient, u
             response = api_client.post(
                 url,
                 params={"username": "valid_user", "invite_code": "expired_code"},
    -            json={"username": "valid_user", "new_password": "pass"},
    +            json={"username": "valid_user", "new_password": "Testpassword1!"},
             )
             assert response.status_code == HTTP_400_BAD_REQUEST
             assert response.json()["detail"] == "Invite code has expired."
    @@ -1954,7 +1994,7 @@ def test_accept_invite_nonexistent_user(self, api_client, url):
                 params={"username": "nonexistent_user", "invite_code": "some_code"},
                 json={
                     "username": "nonexistent_user",
    -                "new_password": "pass",
    +                "new_password": "Testpassword1!",
                 },
             )
             assert response.status_code == HTTP_404_NOT_FOUND
    

Vulnerability mechanics

Generated by null/stub 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.