fix: validate stored MCP tool URLs (#6826)

This commit is contained in:
Hinotobi
2026-04-28 18:29:13 +08:00
committed by GitHub
parent c483a56373
commit c1c6b9520d
8 changed files with 39 additions and 13 deletions
+7
View File
@@ -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({
+13 -1
View File
@@ -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