Skip to content

Commit 7a2774d

Browse files
committed
feat: csrf, two tokens verify
1 parent 7be8ece commit 7a2774d

File tree

20 files changed

+220
-15
lines changed

20 files changed

+220
-15
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import type { NextApiRequest, NextApiResponse } from 'next';
2+
import { verifyCsrfToken } from '../../support/permission/auth/common';
3+
import { generateCsrfToken } from '../../../../projects/app/src/web/support/user/api';
4+
5+
export const withCSRFCheck = async (
6+
req: NextApiRequest,
7+
res: NextApiResponse,
8+
isCSRFCheck: boolean = true
9+
) => {
10+
if (!isCSRFCheck) return;
11+
12+
try {
13+
const csrfToken = await getCsrfTokenFromRequest(req);
14+
verifyCsrfToken(csrfToken || '');
15+
} catch (error) {
16+
return res.status(403).json({
17+
code: 403,
18+
message: 'Invalid CSRF token'
19+
});
20+
}
21+
};
22+
23+
async function getCsrfTokenFromRequest(req: NextApiRequest): Promise<string | null> {
24+
const headerToken = req.headers['x-csrf-token'];
25+
26+
if (!headerToken || typeof headerToken !== 'string') {
27+
const { csrfToken } = await generateCsrfToken();
28+
return csrfToken;
29+
}
30+
31+
return headerToken;
32+
}

packages/service/common/middle/entry.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,31 +3,46 @@ import type { NextApiRequest, NextApiResponse } from 'next';
33
import { withNextCors } from './cors';
44
import { type ApiRequestProps } from '../../type/next';
55
import { addLog } from '../system/log';
6+
import { withCSRFCheck } from './csrf';
67

78
export type NextApiHandler<T = any> = (
89
req: ApiRequestProps,
910
res: NextApiResponse<T>
1011
) => unknown | Promise<unknown>;
1112

13+
type NextAPIOptsType = {
14+
isCSRFCheck: boolean;
15+
};
16+
type Args = [...NextApiHandler[], NextAPIOptsType] | NextApiHandler[];
17+
1218
export const NextEntry = ({
1319
beforeCallback = []
1420
}: {
1521
beforeCallback?: ((req: NextApiRequest, res: NextApiResponse) => Promise<any>)[];
1622
}) => {
17-
return (...args: NextApiHandler[]): NextApiHandler => {
23+
return (...args: Args): NextApiHandler => {
24+
const opts = (() => {
25+
if (typeof args.at(-1) === 'function') {
26+
return {
27+
isCSRFCheck: true
28+
} as NextAPIOptsType;
29+
}
30+
return args.at(-1) as NextAPIOptsType;
31+
})();
1832
return async function api(req: ApiRequestProps, res: NextApiResponse) {
1933
const start = Date.now();
2034
addLog.debug(`Request start ${req.url}`);
2135

2236
try {
2337
await Promise.all([
2438
withNextCors(req, res),
39+
withCSRFCheck(req, res, opts.isCSRFCheck),
2540
...beforeCallback.map((item) => item(req, res))
2641
]);
2742

2843
let response = null;
2944
for await (const handler of args) {
30-
response = await handler(req, res);
45+
if (typeof handler === 'function') response = await handler(req, res);
3146
if (res.writableFinished) {
3247
break;
3348
}

packages/service/support/permission/auth/common.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { ERROR_ENUM } from '@fastgpt/global/common/error/errorCode';
88
import { authUserSession } from '../../../support/user/session';
99
import { authOpenApiKey } from '../../../support/openapi/auth';
1010
import { AuthUserTypeEnum } from '@fastgpt/global/support/permission/constant';
11+
import jwt from 'jsonwebtoken';
1112

1213
export const authCert = async (props: AuthModeType) => {
1314
const result = await parseHeaderCert(props);
@@ -171,3 +172,36 @@ export const setCookie = (res: NextApiResponse, token: string) => {
171172
export const clearCookie = (res: NextApiResponse) => {
172173
res.setHeader('Set-Cookie', `${TokenName}=; Path=/; Max-Age=0`);
173174
};
175+
176+
/* CSRF token */
177+
export const CsrfTokenName = 'csrf_token';
178+
179+
/* set CSRF cookie */
180+
export const setCsrfCookie = (res: NextApiResponse, token: string) => {
181+
res.setHeader(
182+
'Set-Cookie',
183+
`${CsrfTokenName}=${token}; Path=/; HttpOnly; Max-Age=3600; Samesite=Strict;`
184+
);
185+
};
186+
187+
/* clear CSRF cookie */
188+
export const clearCsrfCookie = (res: NextApiResponse) => {
189+
res.setHeader('Set-Cookie', `${CsrfTokenName}=; Path=/; Max-Age=0`);
190+
};
191+
192+
/* verify CSRF JWT token */
193+
export const verifyCsrfToken = (token: string): any => {
194+
try {
195+
const jwtSecret = process.env.TOKEN_KEY || 'any';
196+
const decoded = jwt.verify(token, jwtSecret, { algorithms: ['HS256'] });
197+
198+
if (typeof decoded === 'object' && decoded.type === 'csrf') {
199+
return decoded;
200+
}
201+
202+
throw new Error('Invalid CSRF token type');
203+
} catch (error) {
204+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
205+
throw new Error(`CSRF token verification failed: ${errorMessage}`);
206+
}
207+
};

projects/app/src/components/Markdown/index.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,8 +183,7 @@ const MarkdownRender = ({
183183
'base',
184184
'form',
185185
'input',
186-
'button',
187-
'img'
186+
'button'
188187
]
189188
}
190189
]

projects/app/src/pages/api/common/file/upload.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
113113
removeFilesByPaths(filePaths);
114114
}
115115

116-
export default NextAPI(handler);
116+
export default NextAPI(handler, { isCSRFCheck: false });
117117

118118
export const config = {
119119
api: {

projects/app/src/pages/api/common/file/uploadImage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse): Promise<strin
1313
return uploadMongoImg({ teamId, ...body });
1414
}
1515

16-
export default NextAPI(handler);
16+
export default NextAPI(handler, { isCSRFCheck: false });
1717

1818
export const config = {
1919
api: {

projects/app/src/pages/api/core/app/exportChatLogs.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -417,5 +417,6 @@ async function handler(req: ApiRequestProps<ExportChatLogsBody, {}>, res: NextAp
417417

418418
export default NextAPI(
419419
useIPFrequencyLimit({ id: 'export-chat-logs', seconds: 60, limit: 1, force: true }),
420-
handler
420+
handler,
421+
{ isCSRFCheck: false }
421422
);

projects/app/src/pages/api/core/dataset/collection/create/fileId.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,4 @@ async function handler(
6161
};
6262
}
6363

64-
export default NextAPI(handler);
64+
export default NextAPI(handler, { isCSRFCheck: false });

projects/app/src/pages/api/core/dataset/collection/create/images.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ async function handler(
9595
}
9696
}
9797

98-
export default NextAPI(handler);
98+
export default NextAPI(handler, { isCSRFCheck: false });
9999

100100
export const config = {
101101
api: {

projects/app/src/pages/api/core/dataset/collection/create/localFile.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,4 +88,4 @@ export const config = {
8888
}
8989
};
9090

91-
export default NextAPI(handler);
91+
export default NextAPI(handler, { isCSRFCheck: false });

0 commit comments

Comments
 (0)