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

fixed 渗透测试问题:用户名密码枚举和SSRF #3251

Open
wants to merge 1 commit into
base: main
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
57 changes: 57 additions & 0 deletions packages/service/common/system/log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import chalk from 'chalk';
import { LogLevelEnum } from './log/constant';
import { connectionMongo } from '../mongo/index';
import { getMongoLog } from './log/schema';
import { LoginStatusEnum, LoginTypeEnum } from './loginLog/constant';
import { getLoginLog } from './loginLog/schema';

const logMap = {
[LogLevelEnum.debug]: {
Expand Down Expand Up @@ -90,3 +92,58 @@ export const addLog = {
});
}
};

/* add login logger */
export const addLoginLog = {
Copy link
Collaborator

Choose a reason for hiding this comment

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

有专门的 log 函数,不需要单独写

log(userName: string, type: LoginTypeEnum, status: LoginStatusEnum, message: string) {
console.log(`${userName} ${dayjs().format('YYYY-MM-DD HH:mm:ss')} ${type} ${status}`);
// store
getLoginLog().create({
userName: userName,
type: type,
status: status,
message: message
});
},
login(userName: string, status: LoginStatusEnum, message: string) {
this.log(userName, LoginTypeEnum.login, status, message);
},
logout(userName: string, status: LoginStatusEnum, message: string) {
this.log(userName, LoginTypeEnum.logout, status, message);
}
};

/* check15分钟内失败的登录记录,5条以上就锁定账号 */
export default async function isLoginLocked(userName: string) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

不属于 log 的模块,这个应该属于,login 模块的代码,并且不会复用。只需要放在 login api 里即可

try {
var fifteenMinutesAgo = new Date(new Date().getTime() - 15 * 60 * 1000);
const listParam: any = {
$and: [
{ userName: userName },
{ type: LoginTypeEnum.login },
{ time: { $gt: fifteenMinutesAgo } }
]
};
// 分页查询
const loginLogList = await getLoginLog()
Copy link
Collaborator

Choose a reason for hiding this comment

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

可以直接用 TmpDataSchema 表,设置 ttl 为 15 分钟过期。data 里记录登录次数即可。不需要写这么复杂

.find(listParam)
.skip(0)
.limit(5)
.sort({
time: -1
})
.exec();
if (loginLogList.length < 5) {
return false;
}
for (let i = 0; i < loginLogList.length; i++) {
if (loginLogList[i].status === LoginStatusEnum.success) {
return false;
}
}
return true;
} catch (err) {
console.error(err);
return false;
}
}
9 changes: 9 additions & 0 deletions packages/service/common/system/loginLog/constant.ts
Copy link
Collaborator

Choose a reason for hiding this comment

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

登录错误,属于用户层错误,错误信息统一在 UserErrEnum 枚举中,且报错也在枚举中声明

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export enum LoginStatusEnum {
success = 'success',
failure = 'failure'
}

export enum LoginTypeEnum {
login = 'login',
logout = 'logout'
}
44 changes: 44 additions & 0 deletions packages/service/common/system/loginLog/schema.ts
Copy link
Collaborator

Choose a reason for hiding this comment

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

我们有专门的 TmpDataSchema 表用于存放临时数据,不需要单独建表

Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { getMongoModel, Schema } from '../../../common/mongo';
import { LoginLogType } from './type';
import { LoginStatusEnum, LoginTypeEnum } from './constant';

export const LoginLogCollectionName = 'system_login_logs';

export const getLoginLog = () => {
const LoginLogSchema = new Schema({
userName: {
type: String,
required: true
},
status: {
type: String,
required: true,
enum: Object.values(LoginStatusEnum)
},
type: {
type: String,
required: true,
enum: Object.values(LoginTypeEnum)
},
msg: {
type: String
},
ip: {
type: String
},
client: {
type: String
},
time: {
type: Date,
required: true,
default: () => new Date()
},
metadata: Object
});

LoginLogSchema.index({ time: 1 }, { expires: '7d' });
LoginLogSchema.index({ userName: 1 });

return getMongoModel<LoginLogType>(LoginLogCollectionName, LoginLogSchema);
};
12 changes: 12 additions & 0 deletions packages/service/common/system/loginLog/type.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { LoginStatusEnum, LoginTypeEnum } from './constant';

export type LoginLogType = {
_id: string;
userName: string;
status: LoginStatusEnum;
type: LoginTypeEnum;
msg: string;
ip: string;
client: string;
time: Date;
};
16 changes: 13 additions & 3 deletions projects/app/src/pages/api/support/user/account/loginByPassword.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { connectToDatabase } from '@/service/mongo';
import { getUserDetail } from '@fastgpt/service/support/user/controller';
import type { PostLoginProps } from '@fastgpt/global/support/user/api.d';
import { UserStatusEnum } from '@fastgpt/global/support/user/constant';
import isLoginLocked, { addLoginLog } from '@fastgpt/service/common/system/log';
import { LoginStatusEnum } from '@fastgpt/service/common/system/loginLog/constant';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
Expand All @@ -24,22 +26,30 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
'status'
);
if (!authCert) {
throw new Error('用户未注册');
//用户未注册
throw new Error('用户名或密码错误');
}

if (authCert.status === UserStatusEnum.forbidden) {
addLoginLog.login(username, LoginStatusEnum.failure, '账号已停用');
throw new Error('账号已停用,无法登录');
}

const isLocked = await isLoginLocked(username);
if (isLocked) {
throw new Error('登录失败超过5次,用户已锁定,请15分钟后再试');
}
const user = await MongoUser.findOne({
username,
password
});

if (!user) {
throw new Error('密码错误');
addLoginLog.login(username, LoginStatusEnum.failure, '密码错误');
//密码错误
throw new Error('用户名或密码错误');
}

addLoginLog.login(username, LoginStatusEnum.success, '登录成功');
const userDetail = await getUserDetail({
tmbId: user?.lastLoginTmbId,
userId: user._id
Expand Down
12 changes: 11 additions & 1 deletion projects/app/src/pages/api/support/user/account/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,25 @@ import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSc
/* update user info */
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import { imageBaseUrl } from '@fastgpt/global/common/file/image/constants';
export type UserAccountUpdateQuery = {};
export type UserAccountUpdateBody = UserUpdateParams;
export type UserAccountUpdateResponse = {};
async function handler(
req: ApiRequestProps<UserAccountUpdateBody, UserAccountUpdateQuery>,
_res: ApiResponseType<any>
): Promise<UserAccountUpdateResponse> {
const { avatar, timezone, openaiAccount, lafAccount } = req.body;
let { avatar, timezone, openaiAccount, lafAccount } = req.body;

//校验头像路径格式
if (avatar) {
var regex = /\.(png|jpe?g|gif|svg)(\?.*)?$/;
Copy link
Collaborator

Choose a reason for hiding this comment

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

此处应该封装一个通用的系统图片链接校验方法,通过校验后缀以及以及 baseurl 是否为系统预设前缀。
此外,非特殊情况,不要使用 var

var result = regex.test(avatar);
if (!result) {
throw new Error('头像路径格式不正确');
Copy link
Collaborator

Choose a reason for hiding this comment

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

所有文案都需要经过 i18nT 加工,进行国际化翻译。

}
avatar = imageBaseUrl + avatar;
}
const { tmbId } = await authCert({ req, authToken: true });
const tmb = await MongoTeamMember.findById(tmbId);
if (!tmb) {
Expand Down
2 changes: 2 additions & 0 deletions projects/app/src/web/support/user/useUserStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { TeamMemberItemType } from '@fastgpt/global/support/user/team/type';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { MemberGroupListType } from '@fastgpt/global/support/permission/memberGroup/type';
import { getGroupList } from './team/group/api';
import { imageBaseUrl } from '@fastgpt/global/common/file/image/constants';

type State = {
systemMsgReadId: string;
Expand Down Expand Up @@ -73,6 +74,7 @@ export const useUserStore = create<State>()(
};
});
try {
user.avatar = user.avatar?.replace(imageBaseUrl, '');
Copy link
Collaborator

Choose a reason for hiding this comment

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

前端是不需要做任何操作,后端校验链接来源即可

await putUserInfo(user);
} catch (error) {
set((state) => {
Expand Down
Loading