Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

draft: 修复忘记密码,用户名爆破安全漏洞 #1846

Open
wants to merge 1 commit into
base: release/2.5.3
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions src/api/bkuser_core/api/web/password/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,8 @@ class PasswordResetSendSMSInputSLZ(serializers.Serializer):
telephone = serializers.CharField(required=True, max_length=32)


class PasswordResetSendSMSOutputSLZ(serializers.Serializer):
verification_code_token = serializers.CharField(required=True, max_length=254)
telephone = serializers.CharField(required=True, max_length=16)


class PasswordVerifyVerificationCodeInputSLZ(serializers.Serializer):
verification_code_token = serializers.CharField(required=True, max_length=254)
telephone = serializers.CharField(required=True, max_length=254)
verification_code = serializers.CharField(required=True)


Expand Down
19 changes: 13 additions & 6 deletions src/api/bkuser_core/api/web/password/verification_code_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,15 +80,18 @@ def _check_send_count_exceeded_limit(self):
if send_count > limit_send_count:
raise error_codes.VERIFICATION_CODE_SEND_REACH_LIMIT

def generate_reset_password_token(self, profile_id) -> str:
def _generate_token(self):
hashed_value = f"{self.profile.username}@{self.profile.domain}|{self.profile.telephone}"
md = hashlib.md5()
md.update(hashed_value.encode("utf-8"))
return md.hexdigest()

def generate_reset_password_verification_code(self, profile_id) -> str:
self.profile = Profile.objects.get(id=profile_id)
self.config_loader = ConfigProvider(category_id=self.profile.category_id)

# token 生成
hashed_value = f"{self.profile.username}@{self.profile.domain}|{self.profile.telephone}"
md = hashlib.md5()
md.update(hashed_value.encode("utf-8"))
token = md.hexdigest()
token = self._generate_token()

# 是否已经发送,是否超过当日发送次数
self._check_repeat_send_require(token)
Expand Down Expand Up @@ -136,7 +139,11 @@ def generate_reset_password_token(self, profile_id) -> str:

return token

def verify_verification_code(self, verification_code_token: str, verification_code: str) -> int:
def verify_verification_code(self, profile_id: int, verification_code: str) -> int:
self.profile = Profile.objects.get(id=profile_id)
self.config_loader = ConfigProvider(category_id=self.profile.category_id)

verification_code_token = self._generate_token()
verification_code_data_bytes = self._get_from_cache(verification_code_token, prefix="reset_password")

# token 校验
Expand Down
70 changes: 54 additions & 16 deletions src/api/bkuser_core/api/web/password/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
PasswordResetByTokenInputSLZ,
PasswordResetSendEmailInputSLZ,
PasswordResetSendSMSInputSLZ,
PasswordResetSendSMSOutputSLZ,
PasswordVerifyVerificationCodeInputSLZ,
PasswordVerifyVerificationCodeOutputSLZ,
)
Expand Down Expand Up @@ -93,7 +92,7 @@ def post(self, request, *args, **kwargs):
profile.username,
)

return Response(status=status.HTTP_200_OK)
return Response(status=status.HTTP_204_NO_CONTENT)


class PasswordResetByTokenApi(generics.CreateAPIView):
Expand Down Expand Up @@ -261,21 +260,15 @@ def post(self, request, *args, **kwargs):
logger.exception("failed to get profile by username<%s> because of username format error", input_telephone)
raise error_codes.USERNAME_FORMAT_ERROR

# 生成verification_code_token
verification_code_token = ResetPasswordVerificationCodeHandler().generate_reset_password_token(profile.id)
raw_telephone = profile.telephone

# 用户未绑定手机号,即使用户名就是手机号码
raw_telephone = profile.telephone
if not raw_telephone:
raise error_codes.TELEPHONE_NOT_PROVIDED

response_data = {
"verification_code_token": verification_code_token,
# 加密返回手机号
"telephone": raw_telephone.replace(raw_telephone[3:7], '****'),
}
# 生成verification_code_token
ResetPasswordVerificationCodeHandler().generate_reset_password_verification_code(profile.id)

return Response(PasswordResetSendSMSOutputSLZ(response_data).data)
return Response()
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

一些针对input_telephone的校验异常接口返回,是否也直接返回200。和邮件发送一样,保留校验异常的日志,返回200



class PasswordVerifyVerificationCodeApi(generics.CreateAPIView):
Expand All @@ -285,12 +278,57 @@ def post(self, request, *args, **kwargs):

data = slz.validated_data

input_telephone = data["telephone"]

# 根据交互设计,和登录一样:只能猜测这里传输的username,还是telephone
# 存在着username=telephone的情况
try:
# 优先过滤username
username, domain = parse_username_domain(input_telephone)
if not domain:
domain = ProfileCategory.objects.get_default().domain

# filter过滤,判断是否存在,存在则仅有一个
profile = get_profile_by_username(username, domain)

# 不存在则才是telephone
if not profile:
profile = get_profile_by_telephone(input_telephone)

# 用户状态校验
if not profile.is_normal:
error_msg = (
"failed to send password via sms. "
"profile is abnormal [profile.id=%s, profile.username=%s, profile.enabled=%s, profile.status=%s]"
)

logger.error(
error_msg, profile.id, f"{profile.username}@{profile.domain}", profile.enabled, profile.status
)
raise error_codes.USER_IS_ABNORMAL.f(status=ProfileStatus.get_choice_label(profile.status))

except Profile.DoesNotExist:
logger.exception(
"failed to get profile by telephone<%s> or username<%s>", input_telephone, input_telephone
)
raise error_codes.USER_DOES_NOT_EXIST

except Profile.MultipleObjectsReturned:
logger.exception("this telephone<%s> had bound to multi profiles", input_telephone)
raise error_codes.TELEPHONE_BOUND_TO_MULTI_PROFILE

except Exception:
logger.exception("failed to get profile by username<%s> because of username format error", input_telephone)
raise error_codes.USERNAME_FORMAT_ERROR

# 用户未绑定手机号,即使用户名就是手机号码
if not profile.telephone:
raise error_codes.TELEPHONE_NOT_PROVIDED

verification_code_handler = ResetPasswordVerificationCodeHandler()
profile_id = verification_code_handler.verify_verification_code(profile.id, data["verification_code"])

profile_id = verification_code_handler.verify_verification_code(
data["verification_code_token"], data["verification_code"]
)
profile_token = verification_code_handler.generate_profile_token(profile_id)
# 前端拿到token,作为query_params,拼接重置页面路由
profile_token = verification_code_handler.generate_profile_token(profile_id)
response_data = {"token": profile_token.token}
return Response(PasswordVerifyVerificationCodeOutputSLZ(response_data).data)
14 changes: 5 additions & 9 deletions src/pages/src/views/password/Sms.vue
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
</bk-button>
</template>
<template v-if="step === 'sendCode'">
<p :class="['text', { 'show-error-info': isError }]">{{$t('已向')}}{{simplePhone}}{{ $t('发送验证码') }}</p>
<p :class="['text', { 'show-error-info': isError }]">{{$t('已向')}}{{telephone}}{{ $t('发送验证码') }}</p>
<p class="error-text" v-if="isError">
<i class="icon icon-user-exclamation-circle-shape"></i>
<span class="text">{{errorMessage}}</span>
Expand Down Expand Up @@ -79,9 +79,7 @@ export default {
return {
isError: false,
telephone: '',
simplePhone: '',
verificationCode: '',
verificationCodeToken: '',
step: 'sendSms',
hasReset: false,
remainTime: 0,
Expand All @@ -99,11 +97,9 @@ export default {
async sendSms() {
try {
const telephoneParams = { telephone: this.telephone };
const { result, message, data } = await this.$store.dispatch('password/sendSms', telephoneParams);
if (result) {
const response = await this.$store.dispatch('password/sendSms', telephoneParams);
if (response.result) {
this.step = 'sendCode';
this.simplePhone = data.telephone;
this.verificationCodeToken = data.verification_code_token;
this.remainTime = 60; // 1分钟
// 验证码倒计时
clearInterval(this.timer);
Expand All @@ -115,7 +111,7 @@ export default {
message: this.$t('发送成功'),
});
} else {
this.showErrorMessage(message);
this.showErrorMessage(response.message);
}
} catch (e) {
console.warn(e);
Expand All @@ -124,7 +120,7 @@ export default {
async sendCode() {
try {
const codeParams = {
verification_code_token: this.verificationCodeToken,
telephone: this.telephone,
verification_code: this.verificationCode,
};
const { result, message, data } = await this.$store.dispatch('password/sendCode', codeParams);
Expand Down