|
58 | 58 |
|
59 | 59 | # 验证码校验相关的安全限制 |
60 | 60 | MAX_VERIFY_CODE_ATTEMPTS = 5 |
61 | | -VERIFY_CODE_EXPIRE_SECONDS = 60 * 30 |
62 | | -VERIFY_CODE_FAILED_ATTEMPTS = VERIFY_CODE_EXPIRE_SECONDS |
| 61 | +VERIFY_CODE_EXPIRE_SECONDS = 10 * 60 |
| 62 | +# 达到错误上限后的锁定冷却时长 |
| 63 | +VERIFY_CODE_LOCKOUT_SECONDS = 10 * 60 |
63 | 64 |
|
64 | 65 |
|
65 | 66 | def check_verify_code_attempts(email: str, type_code: str, submitted_code: str) -> bool: |
66 | 67 | """ |
67 | 68 | 校验验证码并限制错误尝试次数,防止验证码被暴力破解(CWE-307)。 |
68 | | - 连续错误达到上限后,使当前验证码立即失效,必须重新发送验证码。 |
| 69 | + 失败计数按邮箱累计且不随重新发送验证码清零:连续错误达到上限后,进入固定冷却期的 |
| 70 | + 锁定,锁定期间即使验证码正确也一律拒绝,必须等待冷却期结束才能重新尝试,从而 |
| 71 | + 避免通过反复发送验证码维持无限猜解节奏。 |
69 | 72 | 校验通过时返回 True,否则抛出校验异常。 |
70 | 73 | """ |
71 | 74 | code_cache_key = email + ":" + type_code |
72 | 75 | failed_cache_key = code_cache_key + "_failed_attempts" |
| 76 | + lock_cache_key = code_cache_key + "_locked" |
73 | 77 | cached_code = cache.get(get_key(code_cache_key), version=version) |
74 | 78 | failed_attempts = int(cache.get(get_key(failed_cache_key), version=version) or 0) |
75 | 79 |
|
76 | | - # 已锁定:验证码已被置为失效,要求重新发送 |
77 | | - if failed_attempts >= MAX_VERIFY_CODE_ATTEMPTS: |
| 80 | + # 已进入锁定冷却期(独立锁 key,固定 10 分钟):无论验证码是否正确都拒绝, |
| 81 | + # 且不刷新锁定时长 |
| 82 | + if cache.get(get_key(lock_cache_key), version=version): |
78 | 83 | cache.delete(get_key(code_cache_key), version=version) |
79 | | - raise AppApiException(500, _("Too many verification code attempts, please request a new code")) |
| 84 | + raise AppApiException(500, _("Too many verification code attempts, please try again later")) |
80 | 85 |
|
81 | 86 | if cached_code is None: |
82 | 87 | raise ExceptionCodeConstants.CODE_ERROR.value.to_app_api_exception() |
83 | 88 |
|
84 | 89 | if cached_code != submitted_code: |
85 | 90 | failed_attempts += 1 |
86 | | - cache.set(get_key(failed_cache_key), failed_attempts, timeout=VERIFY_CODE_FAILED_ATTEMPTS, version=version) |
| 91 | + cache.set(get_key(failed_cache_key), failed_attempts, |
| 92 | + timeout=VERIFY_CODE_LOCKOUT_SECONDS, version=version) |
87 | 93 | if failed_attempts >= MAX_VERIFY_CODE_ATTEMPTS: |
88 | | - # 达到最大尝试次数,立即使验证码失效并进入锁定状态 |
| 94 | + # 错满 5 次:验证码立即失效,并写入独立锁 key 进入固定 10 分钟锁定 |
89 | 95 | cache.delete(get_key(code_cache_key), version=version) |
90 | | - raise AppApiException(500, _("Too many verification code attempts, please request a new code")) |
| 96 | + cache.set(get_key(lock_cache_key), True, timeout=VERIFY_CODE_LOCKOUT_SECONDS, version=version) |
| 97 | + raise AppApiException(500, _("Too many verification code attempts, please try again later")) |
91 | 98 | raise ExceptionCodeConstants.CODE_ERROR.value.to_app_api_exception() |
92 | 99 |
|
93 | | - # 校验通过,清除错误尝试计数 |
| 100 | + # 校验通过,清除错误尝试计数与锁定 |
94 | 101 | cache.delete(get_key(failed_cache_key), version=version) |
| 102 | + cache.delete(get_key(lock_cache_key), version=version) |
95 | 103 | return True |
96 | 104 |
|
97 | 105 |
|
@@ -1149,6 +1157,10 @@ def is_valid(self, *, raise_exception=False): |
1149 | 1157 | if ttl is not None and ttl > 0: |
1150 | 1158 | raise AppApiException(500, _("Do not send emails again within {seconds} seconds").format( |
1151 | 1159 | seconds=int(ttl.total_seconds()))) |
| 1160 | + # 若邮箱处于验证码锁定冷却期,拒绝继续发送,避免验证码邮件轰炸 |
| 1161 | + lock_cache_key = code_cache_key + "_locked" |
| 1162 | + if cache.get(get_key(lock_cache_key), version=version): |
| 1163 | + raise AppApiException(500, _("Too many verification code attempts, please try again later")) |
1152 | 1164 | return True |
1153 | 1165 |
|
1154 | 1166 | def send(self): |
@@ -1198,7 +1210,6 @@ def send(self): |
1198 | 1210 | cache.delete(get_key(code_cache_key_lock)) |
1199 | 1211 | return True |
1200 | 1212 | 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) |
1202 | 1213 | return True |
1203 | 1214 |
|
1204 | 1215 |
|
|
0 commit comments