fix vulnerability (#5098)

* safe

* add get cookie

* fix

* fix

* fix
This commit is contained in:
heheer
2025-06-27 14:35:38 +08:00
committed by GitHub
parent 1cc86f9eb7
commit b6a258d494
9 changed files with 101 additions and 22 deletions

View File

@@ -1,4 +1,33 @@
// Function to escape CSV fields to prevent injection attacks
export const sanitizeCsvField = (field: String): string => {
if (field == null) return '';
let fieldStr = String(field);
// Check for dangerous starting characters that could cause CSV injection
if (fieldStr.match(/^[\=\+\-\@\|]/)) {
// Add prefix to neutralize potential formula injection
fieldStr = `'${fieldStr}`;
}
// Handle special characters that need escaping in CSV
if (
fieldStr.includes(',') ||
fieldStr.includes('"') ||
fieldStr.includes('\n') ||
fieldStr.includes('\r')
) {
// Escape quotes and wrap field in quotes
fieldStr = `"${fieldStr.replace(/"/g, '""')}"`;
}
return fieldStr;
};
export const generateCsv = (headers: string[], data: string[][]) => { export const generateCsv = (headers: string[], data: string[][]) => {
const csv = [headers.join(','), ...data.map((row) => row.join(','))].join('\n'); const sanitizedHeaders = headers.map((header) => sanitizeCsvField(header));
const sanitizedData = data.map((row) => row.map((cell) => sanitizeCsvField(cell)));
const csv = [sanitizedHeaders.join(','), ...sanitizedData.map((row) => row.join(','))].join('\n');
return csv; return csv;
}; };

View File

@@ -1,7 +1,7 @@
import Cookie from 'cookie'; import Cookie from 'cookie';
import { ERROR_ENUM } from '@fastgpt/global/common/error/errorCode'; import { ERROR_ENUM } from '@fastgpt/global/common/error/errorCode';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { type NextApiResponse } from 'next'; import { type NextApiResponse, type NextApiRequest } from 'next';
import type { AuthModeType, ReqHeaderAuthType } from './type.d'; import type { AuthModeType, ReqHeaderAuthType } from './type.d';
import type { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant'; import type { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant';
import { AuthUserTypeEnum } from '@fastgpt/global/support/permission/constant'; import { AuthUserTypeEnum } from '@fastgpt/global/support/permission/constant';
@@ -231,7 +231,7 @@ export async function parseHeaderCert({
return Promise.reject(ERROR_ENUM.unAuthorization); return Promise.reject(ERROR_ENUM.unAuthorization);
} }
return authUserSession(cookieToken); return { ...(await authUserSession(cookieToken)), sessionId: cookieToken };
} }
// from authorization get apikey // from authorization get apikey
async function parseAuthorization(authorization?: string) { async function parseAuthorization(authorization?: string) {
@@ -283,7 +283,7 @@ export async function parseHeaderCert({
const { cookie, token, rootkey, authorization } = (req.headers || {}) as ReqHeaderAuthType; const { cookie, token, rootkey, authorization } = (req.headers || {}) as ReqHeaderAuthType;
const { uid, teamId, tmbId, appId, openApiKey, authType, isRoot, sourceName } = const { uid, teamId, tmbId, appId, openApiKey, authType, isRoot, sourceName, sessionId } =
await (async () => { await (async () => {
if (authApiKey && authorization) { if (authApiKey && authorization) {
// apikey from authorization // apikey from authorization
@@ -309,7 +309,8 @@ export async function parseHeaderCert({
appId: '', appId: '',
openApiKey: '', openApiKey: '',
authType: AuthUserTypeEnum.token, authType: AuthUserTypeEnum.token,
isRoot: res.isRoot isRoot: res.isRoot,
sessionId: res.sessionId
}; };
} }
if (authRoot && rootkey) { if (authRoot && rootkey) {
@@ -341,7 +342,8 @@ export async function parseHeaderCert({
authType, authType,
sourceName, sourceName,
apikey: openApiKey, apikey: openApiKey,
isRoot: !!isRoot isRoot: !!isRoot,
sessionId
}; };
} }
@@ -353,6 +355,7 @@ export const setCookie = (res: NextApiResponse, token: string) => {
`${TokenName}=${token}; Path=/; HttpOnly; Max-Age=604800; Samesite=Strict;` `${TokenName}=${token}; Path=/; HttpOnly; Max-Age=604800; Samesite=Strict;`
); );
}; };
/* clear cookie */ /* clear cookie */
export const clearCookie = (res: NextApiResponse) => { export const clearCookie = (res: NextApiResponse) => {
res.setHeader('Set-Cookie', `${TokenName}=; Path=/; Max-Age=0`); res.setHeader('Set-Cookie', `${TokenName}=; Path=/; Max-Age=0`);

View File

@@ -83,12 +83,11 @@ const getSession = async (key: string): Promise<SessionType> => {
return Promise.reject(ERROR_ENUM.unAuthorization); return Promise.reject(ERROR_ENUM.unAuthorization);
} }
}; };
export const delUserAllSession = async (userId: string, whiteList?: (string | undefined)[]) => {
export const delUserAllSession = async (userId: string, whileList?: string[]) => { const formatWhiteList = whiteList?.map((item) => item && getSessionKey(item));
const formatWhileList = whileList?.map((item) => getSessionKey(item));
const redis = getGlobalRedisConnection(); const redis = getGlobalRedisConnection();
const keys = (await getAllKeysByPrefix(`${redisPrefix}${userId}`)).filter( const keys = (await getAllKeysByPrefix(`${redisPrefix}${userId}`)).filter(
(item) => !formatWhileList?.includes(item) (item) => !formatWhiteList?.includes(item)
); );
if (keys.length > 0) { if (keys.length > 0) {

View File

@@ -11,6 +11,35 @@ const nextConfig = {
output: 'standalone', output: 'standalone',
reactStrictMode: isDev ? false : true, reactStrictMode: isDev ? false : true,
compress: true, compress: true,
async headers() {
return [
{
source: '/((?!chat/share$).*)',
headers: [
{
key: 'X-Frame-Options',
value: 'DENY'
},
{
key: 'X-Content-Type-Options',
value: 'nosniff'
},
{
key: 'X-XSS-Protection',
value: '1; mode=block'
},
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin'
},
{
key: 'Permissions-Policy',
value: 'geolocation=(self), microphone=(self), camera=(self)'
}
]
}
];
},
webpack(config, { isServer, nextRuntime }) { webpack(config, { isServer, nextRuntime }) {
Object.assign(config.resolve.alias, { Object.assign(config.resolve.alias, {
'@mongodb-js/zstd': false, '@mongodb-js/zstd': false,
@@ -85,7 +114,7 @@ const nextConfig = {
'pg', 'pg',
'bullmq', 'bullmq',
'@zilliz/milvus2-sdk-node', '@zilliz/milvus2-sdk-node',
"tiktoken", 'tiktoken'
], ],
outputFileTracingRoot: path.join(__dirname, '../../'), outputFileTracingRoot: path.join(__dirname, '../../'),
instrumentationHook: true instrumentationHook: true

View File

@@ -18,9 +18,12 @@ import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSc
import type { ChatSourceEnum } from '@fastgpt/global/core/chat/constants'; import type { ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants'; import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants';
import { type AIChatItemValueItemType } from '@fastgpt/global/core/chat/type'; import { type AIChatItemValueItemType } from '@fastgpt/global/core/chat/type';
import { sanitizeCsvField } from '@fastgpt/service/common/file/csv';
const formatJsonString = (data: any) => { const formatJsonString = (data: any) => {
return JSON.stringify(data).replace(/"/g, '""').replace(/\n/g, '\\n'); if (data == null) return '';
const jsonStr = JSON.stringify(data).replace(/"/g, '""').replace(/\n/g, '\\n');
return sanitizeCsvField(jsonStr);
}; };
export type ExportChatLogsBody = GetAppChatLogsProps & { export type ExportChatLogsBody = GetAppChatLogsProps & {
@@ -258,7 +261,14 @@ async function handler(req: ApiRequestProps<ExportChatLogsBody, {}>, res: NextAp
const markItemsStr = formatJsonString(markItems); const markItemsStr = formatJsonString(markItems);
const chatDetailsStr = formatJsonString(chatDetails); const chatDetailsStr = formatJsonString(chatDetails);
const res = `\n"${time}","${source}","${tmbName}","${tmbContact}","${title}","${messageCount}","${userGoodFeedbackItemsStr}","${userBadFeedbackItemsStr}","${customFeedbackItemsStr}","${markItemsStr}","${chatDetailsStr}"`; const sanitizedTime = sanitizeCsvField(time);
const sanitizedSource = sanitizeCsvField(source);
const sanitizedTmbName = sanitizeCsvField(tmbName);
const sanitizedTmbContact = sanitizeCsvField(tmbContact);
const sanitizedTitle = sanitizeCsvField(title);
const sanitizedMessageCount = sanitizeCsvField(messageCount);
const res = `\n${sanitizedTime},${sanitizedSource},${sanitizedTmbName},${sanitizedTmbContact},${sanitizedTitle},${sanitizedMessageCount},${userGoodFeedbackItemsStr},${userBadFeedbackItemsStr},${customFeedbackItemsStr},${markItemsStr},${chatDetailsStr}`;
write(res); write(res);
}); });

View File

@@ -12,6 +12,7 @@ import { MongoDatasetData } from '@fastgpt/service/core/dataset/data/schema';
import { authDatasetCollection } from '@fastgpt/service/support/permission/dataset/auth'; import { authDatasetCollection } from '@fastgpt/service/support/permission/dataset/auth';
import { type ApiRequestProps } from '@fastgpt/service/type/next'; import { type ApiRequestProps } from '@fastgpt/service/type/next';
import { type NextApiResponse } from 'next'; import { type NextApiResponse } from 'next';
import { sanitizeCsvField } from '@fastgpt/service/common/file/csv';
export type ExportCollectionBody = { export type ExportCollectionBody = {
collectionId: string; collectionId: string;
@@ -109,10 +110,10 @@ async function handler(req: ApiRequestProps<ExportCollectionBody, {}>, res: Next
write(`\uFEFFindex,content`); write(`\uFEFFindex,content`);
cursor.on('data', (doc) => { cursor.on('data', (doc) => {
const q = doc.q.replace(/"/g, '""') || ''; const sanitizedQ = sanitizeCsvField(doc.q || '');
const a = doc.a.replace(/"/g, '""') || ''; const sanitizedA = sanitizeCsvField(doc.a || '');
write(`\n"${q}","${a}"`); write(`\n${sanitizedQ},${sanitizedA}`);
}); });
cursor.on('end', () => { cursor.on('end', () => {

View File

@@ -13,6 +13,7 @@ import { WritePermissionVal } from '@fastgpt/global/support/permission/constant'
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common'; import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
import { readFromSecondary } from '@fastgpt/service/common/mongo/utils'; import { readFromSecondary } from '@fastgpt/service/common/mongo/utils';
import type { DatasetDataSchemaType } from '@fastgpt/global/core/dataset/type'; import type { DatasetDataSchemaType } from '@fastgpt/global/core/dataset/type';
import { sanitizeCsvField } from '@fastgpt/service/common/file/csv';
type DataItemType = { type DataItemType = {
_id: string; _id: string;
@@ -76,11 +77,11 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
write(`\uFEFFq,a,indexes`); write(`\uFEFFq,a,indexes`);
cursor.on('data', (doc: DataItemType) => { cursor.on('data', (doc: DataItemType) => {
const q = doc.q.replace(/"/g, '""') || ''; const sanitizedQ = sanitizeCsvField(doc.q || '');
const a = doc.a.replace(/"/g, '""') || ''; const sanitizedA = sanitizeCsvField(doc.a || '');
const indexes = doc.indexes.map((i) => `"${i.text.replace(/"/g, '""')}"`).join(','); const sanitizedIndexes = doc.indexes.map((i) => sanitizeCsvField(i.text || '')).join(',');
write(`\n"${q}","${a}",${indexes}`); write(`\n${sanitizedQ},${sanitizedA},${sanitizedIndexes}`);
}); });
cursor.on('end', () => { cursor.on('end', () => {

View File

@@ -4,6 +4,7 @@ import { MongoUser } from '@fastgpt/service/support/user/schema';
import { NextAPI } from '@/service/middleware/entry'; import { NextAPI } from '@/service/middleware/entry';
import { i18nT } from '@fastgpt/web/i18n/utils'; import { i18nT } from '@fastgpt/web/i18n/utils';
import { checkPswExpired } from '@/service/support/user/account/password'; import { checkPswExpired } from '@/service/support/user/account/password';
import { delUserAllSession } from '@fastgpt/service/support/user/session';
export type resetExpiredPswQuery = {}; export type resetExpiredPswQuery = {};
@@ -18,7 +19,7 @@ async function resetExpiredPswHandler(
res: ApiResponseType<resetExpiredPswResponse> res: ApiResponseType<resetExpiredPswResponse>
): Promise<resetExpiredPswResponse> { ): Promise<resetExpiredPswResponse> {
const newPsw = req.body.newPsw; const newPsw = req.body.newPsw;
const { userId } = await authCert({ req, authToken: true }); const { userId, sessionId } = await authCert({ req, authToken: true });
const user = await MongoUser.findById(userId, 'passwordUpdateTime').lean(); const user = await MongoUser.findById(userId, 'passwordUpdateTime').lean();
if (!user) { if (!user) {
@@ -43,6 +44,8 @@ async function resetExpiredPswHandler(
} }
); );
await delUserAllSession(userId, [sessionId]);
return {}; return {};
} }

View File

@@ -7,6 +7,8 @@ import { i18nT } from '@fastgpt/web/i18n/utils';
import { NextAPI } from '@/service/middleware/entry'; import { NextAPI } from '@/service/middleware/entry';
import { addAuditLog } from '@fastgpt/service/support/user/audit/util'; import { addAuditLog } from '@fastgpt/service/support/user/audit/util';
import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants'; import { AuditEventEnum } from '@fastgpt/global/support/user/audit/constants';
import { delUserAllSession } from '@fastgpt/service/support/user/session';
import { parseHeaderCert } from '@fastgpt/service/support/permission/controller';
async function handler(req: NextApiRequest, res: NextApiResponse<any>) { async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
const { oldPsw, newPsw } = req.body as { oldPsw: string; newPsw: string }; const { oldPsw, newPsw } = req.body as { oldPsw: string; newPsw: string };
@@ -14,7 +16,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
return Promise.reject('Params is missing'); return Promise.reject('Params is missing');
} }
const { tmbId, teamId } = await authCert({ req, authToken: true }); const { tmbId, teamId, sessionId } = await authCert({ req, authToken: true });
const tmb = await MongoTeamMember.findById(tmbId); const tmb = await MongoTeamMember.findById(tmbId);
if (!tmb) { if (!tmb) {
return Promise.reject('can not find it'); return Promise.reject('can not find it');
@@ -40,6 +42,8 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
passwordUpdateTime: new Date() passwordUpdateTime: new Date()
}); });
await delUserAllSession(userId, [sessionId]);
(async () => { (async () => {
addAuditLog({ addAuditLog({
tmbId, tmbId,