diff --git a/apps/locales/en_US/LC_MESSAGES/django.po b/apps/locales/en_US/LC_MESSAGES/django.po index d8916484dcd..18550687647 100644 --- a/apps/locales/en_US/LC_MESSAGES/django.po +++ b/apps/locales/en_US/LC_MESSAGES/django.po @@ -1724,6 +1724,11 @@ msgstr "" msgid "The verification code is incorrect or the verification code has expired" msgstr "" +#: apps/users/serializers/user.py:79 +#: apps/users/serializers/user.py:90 +msgid "Too many verification code attempts, please request a new code" +msgstr "" + #: apps/common/constants/exception_code_constants.py:39 msgid "The username has been registered, please log in directly" msgstr "" @@ -9387,4 +9392,4 @@ msgid "Token Index" msgstr "Token Index" msgid "Authorize to Workspace" -msgstr "" \ No newline at end of file +msgstr "" diff --git a/apps/locales/zh_CN/LC_MESSAGES/django.po b/apps/locales/zh_CN/LC_MESSAGES/django.po index eb95d977888..c4f6a8cb181 100644 --- a/apps/locales/zh_CN/LC_MESSAGES/django.po +++ b/apps/locales/zh_CN/LC_MESSAGES/django.po @@ -1731,6 +1731,11 @@ msgstr "该邮箱未注册,请先注册" msgid "The verification code is incorrect or the verification code has expired" msgstr "验证码不正确或已过期" +#: apps/users/serializers/user.py:79 +#: apps/users/serializers/user.py:90 +msgid "Too many verification code attempts, please request a new code" +msgstr "验证码错误次数过多,请重新获取验证码" + #: apps/common/constants/exception_code_constants.py:39 msgid "The username has been registered, please log in directly" msgstr "用户名已注册,请直接登录" @@ -9510,4 +9515,4 @@ msgid "Token Index" msgstr "分词索引" msgid "Authorize to Workspace" -msgstr "授权工作空间" \ No newline at end of file +msgstr "授权工作空间" diff --git a/apps/locales/zh_Hant/LC_MESSAGES/django.po b/apps/locales/zh_Hant/LC_MESSAGES/django.po index dc20446d37c..7e89607dc89 100644 --- a/apps/locales/zh_Hant/LC_MESSAGES/django.po +++ b/apps/locales/zh_Hant/LC_MESSAGES/django.po @@ -1731,6 +1731,11 @@ msgstr "該郵箱未註冊,請先註冊" msgid "The verification code is incorrect or the verification code has expired" msgstr "驗證碼不正確或已過期" +#: apps/users/serializers/user.py:79 +#: apps/users/serializers/user.py:90 +msgid "Too many verification code attempts, please request a new code" +msgstr "驗證碼錯誤次數過多,請重新取得驗證碼" + #: apps/common/constants/exception_code_constants.py:39 msgid "The username has been registered, please log in directly" msgstr "用戶名已註冊,請直接登錄" @@ -9510,4 +9515,4 @@ msgid "Token Index" msgstr "分詞索引" msgid "Authorize to Workspace" -msgstr "授權工作空間" \ No newline at end of file +msgstr "授權工作空間" diff --git a/apps/users/serializers/user.py b/apps/users/serializers/user.py index 631543dae03..3ec3b77e7f4 100644 --- a/apps/users/serializers/user.py +++ b/apps/users/serializers/user.py @@ -56,6 +56,45 @@ version, get_key = Cache_Version.SYSTEM.value +# 验证码校验相关的安全限制 +MAX_VERIFY_CODE_ATTEMPTS = 5 +VERIFY_CODE_EXPIRE_SECONDS = 60 * 30 +VERIFY_CODE_FAILED_ATTEMPTS = VERIFY_CODE_EXPIRE_SECONDS + + +def check_verify_code_attempts(email: str, type_code: str, submitted_code: str) -> bool: + """ + 校验验证码并限制错误尝试次数,防止验证码被暴力破解(CWE-307)。 + 连续错误达到上限后,使当前验证码立即失效,必须重新发送验证码。 + 校验通过时返回 True,否则抛出校验异常。 + """ + code_cache_key = email + ":" + type_code + failed_cache_key = code_cache_key + "_failed_attempts" + cached_code = cache.get(get_key(code_cache_key), version=version) + failed_attempts = int(cache.get(get_key(failed_cache_key), version=version) or 0) + + # 已锁定:验证码已被置为失效,要求重新发送 + if failed_attempts >= MAX_VERIFY_CODE_ATTEMPTS: + cache.delete(get_key(code_cache_key), version=version) + raise AppApiException(500, _("Too many verification code attempts, please request a new code")) + + if cached_code is None: + raise ExceptionCodeConstants.CODE_ERROR.value.to_app_api_exception() + + if cached_code != submitted_code: + failed_attempts += 1 + cache.set(get_key(failed_cache_key), failed_attempts, timeout=VERIFY_CODE_FAILED_ATTEMPTS, version=version) + if failed_attempts >= MAX_VERIFY_CODE_ATTEMPTS: + # 达到最大尝试次数,立即使验证码失效并进入锁定状态 + cache.delete(get_key(code_cache_key), version=version) + raise AppApiException(500, _("Too many verification code attempts, please request a new code")) + raise ExceptionCodeConstants.CODE_ERROR.value.to_app_api_exception() + + # 校验通过,清除错误尝试计数 + cache.delete(get_key(failed_cache_key), version=version) + return True + + class UserProfileResponse(serializers.ModelSerializer): is_edit_password = serializers.BooleanField(required=True, label=_('Is Edit Password')) permissions = serializers.ListField(required=True, label=_('permissions')) @@ -1016,13 +1055,10 @@ class Meta: def is_valid(self, *, raise_exception=False): super().is_valid(raise_exception=True) email = self.data.get("email") - cache_code = cache.get(get_key(email + ':reset_password'), version=version) if self.data.get('password') != self.data.get('re_password'): raise AppApiException(ExceptionCodeConstants.PASSWORD_NOT_EQ_RE_PASSWORD.value.code, ExceptionCodeConstants.PASSWORD_NOT_EQ_RE_PASSWORD.value.message) - if cache_code != self.data.get('code'): - raise AppApiException(ExceptionCodeConstants.CODE_ERROR.value.code, - ExceptionCodeConstants.CODE_ERROR.value.message) + check_verify_code_attempts(email, "reset_password", self.data.get('code')) return True def reset_password(self): @@ -1161,7 +1197,8 @@ def send(self): except Exception as e: cache.delete(get_key(code_cache_key_lock)) return True - cache.set(get_key(code_cache_key), code, timeout=60 * 30, version=version) + cache.set(get_key(code_cache_key), code, timeout=VERIFY_CODE_EXPIRE_SECONDS, version=version) + cache.delete(get_key(code_cache_key + "_failed_attempts"), version=version) return True @@ -1187,10 +1224,7 @@ class CheckCodeSerializer(serializers.Serializer): def is_valid(self, *, raise_exception=False): super().is_valid() - value = cache.get(get_key(self.data.get("email") + ":" + self.data.get("type")), version=version) - if value is None or value != self.data.get("code"): - raise ExceptionCodeConstants.CODE_ERROR.value.to_app_api_exception() - return True + return check_verify_code_attempts(self.data.get("email"), self.data.get("type"), self.data.get("code")) class SwitchLanguageSerializer(serializers.Serializer):