5656version , get_key = Cache_Version .SYSTEM .value
5757
5858
59+ # 验证码校验相关的安全限制
60+ MAX_VERIFY_CODE_ATTEMPTS = 5
61+ VERIFY_CODE_EXPIRE_SECONDS = 60 * 30
62+ VERIFY_CODE_FAILED_ATTEMPTS = VERIFY_CODE_EXPIRE_SECONDS
63+
64+
65+ def check_verify_code_attempts (email : str , type_code : str , submitted_code : str ) -> bool :
66+ """
67+ 校验验证码并限制错误尝试次数,防止验证码被暴力破解(CWE-307)。
68+ 连续错误达到上限后,使当前验证码立即失效,必须重新发送验证码。
69+ 校验通过时返回 True,否则抛出校验异常。
70+ """
71+ code_cache_key = email + ":" + type_code
72+ failed_cache_key = code_cache_key + "_failed_attempts"
73+ cached_code = cache .get (get_key (code_cache_key ), version = version )
74+ failed_attempts = int (cache .get (get_key (failed_cache_key ), version = version ) or 0 )
75+
76+ # 已锁定:验证码已被置为失效,要求重新发送
77+ if failed_attempts >= MAX_VERIFY_CODE_ATTEMPTS :
78+ cache .delete (get_key (code_cache_key ), version = version )
79+ raise AppApiException (500 , _ ("Too many verification code attempts, please request a new code" ))
80+
81+ if cached_code is None :
82+ raise ExceptionCodeConstants .CODE_ERROR .value .to_app_api_exception ()
83+
84+ if cached_code != submitted_code :
85+ failed_attempts += 1
86+ cache .set (get_key (failed_cache_key ), failed_attempts , timeout = VERIFY_CODE_FAILED_ATTEMPTS , version = version )
87+ if failed_attempts >= MAX_VERIFY_CODE_ATTEMPTS :
88+ # 达到最大尝试次数,立即使验证码失效并进入锁定状态
89+ cache .delete (get_key (code_cache_key ), version = version )
90+ raise AppApiException (500 , _ ("Too many verification code attempts, please request a new code" ))
91+ raise ExceptionCodeConstants .CODE_ERROR .value .to_app_api_exception ()
92+
93+ # 校验通过,清除错误尝试计数
94+ cache .delete (get_key (failed_cache_key ), version = version )
95+ return True
96+
97+
5998class UserProfileResponse (serializers .ModelSerializer ):
6099 is_edit_password = serializers .BooleanField (required = True , label = _ ('Is Edit Password' ))
61100 permissions = serializers .ListField (required = True , label = _ ('permissions' ))
@@ -1016,13 +1055,10 @@ class Meta:
10161055 def is_valid (self , * , raise_exception = False ):
10171056 super ().is_valid (raise_exception = True )
10181057 email = self .data .get ("email" )
1019- cache_code = cache .get (get_key (email + ':reset_password' ), version = version )
10201058 if self .data .get ('password' ) != self .data .get ('re_password' ):
10211059 raise AppApiException (ExceptionCodeConstants .PASSWORD_NOT_EQ_RE_PASSWORD .value .code ,
10221060 ExceptionCodeConstants .PASSWORD_NOT_EQ_RE_PASSWORD .value .message )
1023- if cache_code != self .data .get ('code' ):
1024- raise AppApiException (ExceptionCodeConstants .CODE_ERROR .value .code ,
1025- ExceptionCodeConstants .CODE_ERROR .value .message )
1061+ check_verify_code_attempts (email , "reset_password" , self .data .get ('code' ))
10261062 return True
10271063
10281064 def reset_password (self ):
@@ -1161,7 +1197,8 @@ def send(self):
11611197 except Exception as e :
11621198 cache .delete (get_key (code_cache_key_lock ))
11631199 return True
1164- cache .set (get_key (code_cache_key ), code , timeout = 60 * 30 , version = version )
1200+ cache .set (get_key (code_cache_key ), code , timeout = VERIFY_CODE_EXPIRE_SECONDS , version = version )
1201+ cache .delete (get_key (code_cache_key + "_failed_attempts" ), version = version )
11651202 return True
11661203
11671204
@@ -1187,10 +1224,7 @@ class CheckCodeSerializer(serializers.Serializer):
11871224
11881225 def is_valid (self , * , raise_exception = False ):
11891226 super ().is_valid ()
1190- value = cache .get (get_key (self .data .get ("email" ) + ":" + self .data .get ("type" )), version = version )
1191- if value is None or value != self .data .get ("code" ):
1192- raise ExceptionCodeConstants .CODE_ERROR .value .to_app_api_exception ()
1193- return True
1227+ return check_verify_code_attempts (self .data .get ("email" ), self .data .get ("type" ), self .data .get ("code" ))
11941228
11951229
11961230class SwitchLanguageSerializer (serializers .Serializer ):
0 commit comments