mirror of
https://github.com/labring/FastGPT.git
synced 2026-05-07 01:02:55 +08:00
fix: validate stored MCP tool URLs (#6826)
This commit is contained in:
@@ -10,9 +10,16 @@ import type { McpToolDataType } from '@fastgpt/global/core/app/tool/mcpTool/type
|
||||
import { UserError } from '@fastgpt/global/common/error/utils';
|
||||
import $RefParser from '@apidevtools/json-schema-ref-parser';
|
||||
import { getLogger, LogCategories } from '../../common/logger';
|
||||
import { isInternalAddress, PRIVATE_URL_TEXT } from '../../common/system/utils';
|
||||
|
||||
const logger = getLogger(LogCategories.MODULE.APP.MCP_TOOLS);
|
||||
|
||||
export const assertMCPUrlNotInternal = async (url: string) => {
|
||||
if (await isInternalAddress(url)) {
|
||||
return Promise.reject(PRIVATE_URL_TEXT);
|
||||
}
|
||||
};
|
||||
|
||||
export class MCPClient {
|
||||
private client: Client;
|
||||
private url: string;
|
||||
|
||||
@@ -16,7 +16,7 @@ import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { pushTrack } from '../../../../../../../common/middle/tracks/utils';
|
||||
import { getErrText } from '@fastgpt/global/common/error/utils';
|
||||
import { getAppVersionById } from '../../../../../../app/version/controller';
|
||||
import { MCPClient } from '../../../../../../app/mcp';
|
||||
import { assertMCPUrlNotInternal, MCPClient } from '../../../../../../app/mcp';
|
||||
import { runHTTPTool } from '../../../../../../app/http';
|
||||
import { getS3ChatSource } from '../../../../../../../common/s3/sources/chat';
|
||||
import { parseToolId } from '../../../../child/runTool';
|
||||
@@ -197,6 +197,9 @@ export const dispatchTool = async ({
|
||||
|
||||
const { headerSecret, url } =
|
||||
tool.nodes[0].toolConfig?.mcpToolSet ?? tool.nodes[0].inputs[0].value;
|
||||
|
||||
await assertMCPUrlNotInternal(url);
|
||||
|
||||
const mcpClient = new MCPClient({
|
||||
url,
|
||||
headers: getSecretValue({
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
type ModuleDispatchProps
|
||||
} from '@fastgpt/global/core/workflow/runtime/type';
|
||||
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { MCPClient } from '../../../app/mcp';
|
||||
import { assertMCPUrlNotInternal, MCPClient } from '../../../app/mcp';
|
||||
import { getSecretValue } from '../../../../common/secret/utils';
|
||||
import type { McpToolDataType } from '@fastgpt/global/core/app/tool/mcpTool/type';
|
||||
import type { HttpToolConfigType } from '@fastgpt/global/core/app/tool/httpTool/type';
|
||||
@@ -214,6 +214,8 @@ export const dispatchRunTool = async (props: RunToolProps): Promise<RunToolRespo
|
||||
const { headerSecret, url } =
|
||||
tool.nodes[0].toolConfig?.mcpToolSet ?? tool.nodes[0].inputs[0].value;
|
||||
|
||||
await assertMCPUrlNotInternal(url);
|
||||
|
||||
const context = getWorkflowContext();
|
||||
// Buffer mcpClient in this workflow
|
||||
const mcpClient =
|
||||
@@ -301,6 +303,8 @@ export const dispatchRunTool = async (props: RunToolProps): Promise<RunToolRespo
|
||||
const { toolData, system_toolData, ...restParams } = params;
|
||||
const { name: toolName, url, headerSecret } = toolData || system_toolData;
|
||||
|
||||
await assertMCPUrlNotInternal(url);
|
||||
|
||||
const mcpClient = new MCPClient({
|
||||
url,
|
||||
headers: getSecretValue({
|
||||
|
||||
@@ -18,7 +18,7 @@ vi.mock('@fastgpt/service/core/app/schema', () => ({
|
||||
}
|
||||
}));
|
||||
|
||||
import { MCPClient, getMCPChildren } from '@fastgpt/service/core/app/mcp';
|
||||
import { MCPClient, assertMCPUrlNotInternal, getMCPChildren } from '@fastgpt/service/core/app/mcp';
|
||||
import type { AppSchemaType } from '@fastgpt/global/core/app/type';
|
||||
|
||||
// Access private client via prototype for spying
|
||||
@@ -38,6 +38,18 @@ beforeEach(() => {
|
||||
describe('MCPClient', () => {
|
||||
const config = { url: 'http://localhost:3000/mcp', headers: { Authorization: 'Bearer test' } };
|
||||
|
||||
describe('assertMCPUrlNotInternal', () => {
|
||||
it('should reject localhost MCP endpoints', async () => {
|
||||
await expect(assertMCPUrlNotInternal('http://localhost:3000/mcp')).rejects.toBe(
|
||||
'Request to private network not allowed'
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow public MCP endpoints', async () => {
|
||||
await expect(assertMCPUrlNotInternal('https://example.com/mcp')).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
// Helper: stub getConnection to avoid real network calls
|
||||
const stubConnection = (mcpClient: MCPClient) => {
|
||||
const client = getPrivateClient(mcpClient);
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
type CreateMcpToolsBodyType,
|
||||
type CreateMcpToolsResponseType
|
||||
} from '@fastgpt/global/openapi/core/app/mcpTools/api';
|
||||
import { assertMCPUrlNotInternal } from '@fastgpt/service/core/app/mcp';
|
||||
|
||||
export type createMCPToolsQuery = {};
|
||||
|
||||
@@ -37,6 +38,8 @@ async function handler(
|
||||
? await authApp({ req, appId: parentId, per: WritePermissionVal, authToken: true })
|
||||
: await authUserPer({ req, authToken: true, per: TeamAppCreatePermissionVal });
|
||||
|
||||
await assertMCPUrlNotInternal(url);
|
||||
|
||||
await checkTeamAppTypeLimit({ teamId, appCheckType: 'tool' });
|
||||
|
||||
const formatedHeaderAuth = storeSecretValue(headerSecret);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
|
||||
import { MCPClient } from '@fastgpt/service/core/app/mcp';
|
||||
import { assertMCPUrlNotInternal, MCPClient } from '@fastgpt/service/core/app/mcp';
|
||||
import { getSecretValue } from '@fastgpt/service/common/secret/utils';
|
||||
import {
|
||||
GetMcpToolsBodySchema,
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
type GetMcpToolsBodyType,
|
||||
type GetMcpToolsResponseType
|
||||
} from '@fastgpt/global/openapi/core/app/mcpTools/api';
|
||||
import { isInternalAddress, PRIVATE_URL_TEXT } from '@fastgpt/service/common/system/utils';
|
||||
import { authCert } from '@fastgpt/service/support/permission/auth/common';
|
||||
|
||||
async function handler(
|
||||
@@ -19,9 +18,7 @@ async function handler(
|
||||
|
||||
const { url, headerSecret } = GetMcpToolsBodySchema.parse(req.body);
|
||||
|
||||
if (await isInternalAddress(url)) {
|
||||
return Promise.reject(PRIVATE_URL_TEXT);
|
||||
}
|
||||
await assertMCPUrlNotInternal(url);
|
||||
|
||||
const mcpClient = new MCPClient({
|
||||
url,
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
|
||||
import { MCPClient } from '@fastgpt/service/core/app/mcp';
|
||||
import { assertMCPUrlNotInternal, MCPClient } from '@fastgpt/service/core/app/mcp';
|
||||
import { getSecretValue } from '@fastgpt/service/common/secret/utils';
|
||||
import {
|
||||
RunMcpToolBodySchema,
|
||||
type RunMcpToolBodyType,
|
||||
type RunMcpToolResponseType
|
||||
} from '@fastgpt/global/openapi/core/app/mcpTools/api';
|
||||
import { isInternalAddress, PRIVATE_URL_TEXT } from '@fastgpt/service/common/system/utils';
|
||||
import { authCert } from '@fastgpt/service/support/permission/auth/common';
|
||||
|
||||
async function handler(
|
||||
@@ -18,9 +17,7 @@ async function handler(
|
||||
|
||||
const { url, toolName, headerSecret, params } = RunMcpToolBodySchema.parse(req.body);
|
||||
|
||||
if (await isInternalAddress(url)) {
|
||||
return Promise.reject(PRIVATE_URL_TEXT);
|
||||
}
|
||||
await assertMCPUrlNotInternal(url);
|
||||
|
||||
const mcpClient = new MCPClient({
|
||||
url,
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
UpdateMcpToolsBodySchema,
|
||||
type UpdateMcpToolsBodyType
|
||||
} from '@fastgpt/global/openapi/core/app/mcpTools/api';
|
||||
import { assertMCPUrlNotInternal } from '@fastgpt/service/core/app/mcp';
|
||||
|
||||
export type updateMCPToolsQuery = {};
|
||||
|
||||
@@ -20,6 +21,8 @@ async function handler(req: ApiRequestProps<UpdateMcpToolsBodyType>, res: ApiRes
|
||||
const { appId, url, toolList, headerSecret } = UpdateMcpToolsBodySchema.parse(req.body);
|
||||
const { app } = await authApp({ req, authToken: true, appId, per: ManagePermissionVal });
|
||||
|
||||
await assertMCPUrlNotInternal(url);
|
||||
|
||||
const formatedHeaderAuth = storeSecretValue(headerSecret);
|
||||
|
||||
// create tool set node
|
||||
|
||||
Reference in New Issue
Block a user