mirror of
https://github.com/labring/FastGPT.git
synced 2025-10-15 15:41:05 +00:00
@@ -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;
|
||||||
};
|
};
|
||||||
|
@@ -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`);
|
||||||
|
@@ -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) {
|
||||||
|
@@ -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
|
||||||
|
@@ -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);
|
||||||
});
|
});
|
||||||
|
@@ -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', () => {
|
||||||
|
@@ -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', () => {
|
||||||
|
@@ -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 {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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,
|
||||||
|
Reference in New Issue
Block a user