High severityOSV Advisory· Published Aug 28, 2025· Updated Apr 15, 2026
CVE-2025-58062
CVE-2025-58062
Description
LSTM-Kirigaya's openmcp-client is a vscode plugin for mcp developer. Prior to version 0.1.12, when users on a Windows platform connect to an attacker controlled MCP server, attackers could provision a malicious authorization server endpoint to silently achieve an OS command injection attack in the open() invocation, leading to client system compromise. This issue has been patched in version 0.1.12.
Affected products
1- Range: v0.0.8, v0.0.9, v0.1.0, …
Patches
292e5e4529a43修复 issue #63:[Bug] 在MCP工作区断开连接后并不会关闭进程
6 files changed · +360 −323
CHANGELOG.md+2 −0 modified@@ -7,6 +7,8 @@ - 修复 uv run 非跟路径的 python 文件的错误。 - 增加清空对话的功能(在切换到并行对话按钮的右侧) https://picx.zhimg.com/80/v2-5d0524df2eabfedd8a5b2cc7229f1f44_1440w.png - 完成 issue #64:[Feature] 新增断开MCP连接功能 https://pica.zhimg.com/80/v2-5822bb01cfd819072dd02cf730815117_1440w.png +- 修复 issue #63:[Bug] 在MCP工作区断开连接后并不会关闭进程 + ## [main] 0.1.11 - 添加新功能:并行对话。在交互测试中,用户可以点击左上角的 switch to single chat 来切换到并行对话窗口,这个窗口,你可以让大模型同时回答同一个问题,用于并行测试。 https://picx.zhimg.com/80/v2-92807279c2dcbbf7808b483f0141f592_1440w.png
service/src/common/router.ts+15 −9 modified@@ -6,6 +6,7 @@ import { ConnectController } from "../mcp/connect.controller.js"; import { OcrController } from "../mcp/ocr.controller.js"; import { PanelController } from "../panel/panel.controller.js"; import { SettingController } from "../setting/setting.controller.js"; +export { disconnectService } from "../mcp/connect.service.js"; export const ModuleControllers = [ ConnectController, @@ -26,22 +27,27 @@ export async function routeMessage(command: string, data: any, webview: PostMess // res.code = -1 代表当前请求不需要返回发送 if (res.code >= 0) { + const payload = { + _id: data._id, + ...res + } webview.postMessage({ - command, data: { - _id: data._id, - ...res - } + command, data: payload }); + return payload; } } catch (error) { // console.error(error); + const payload = { + _id: data._id, + code: 500, + msg: (error as any).toString() + } webview.postMessage({ - command, data: { - _id: data._id, - code: 500, - msg: (error as any).toString() - } + command, data: payload }); + + return payload; } } return
service/src/index.ts+1 −1 modified@@ -1,4 +1,4 @@ -export { routeMessage } from './common/router.js'; +export { routeMessage, disconnectService } from './common/router.js'; export { VSCodeWebViewLike } from './hook/adapter.js'; export { setVscodeWorkspace, setRunningCWD, setDefaultLang } from './hook/setting.js'; export { clientMap } from './mcp/connect.service.js'; \ No newline at end of file
service/src/mcp/connect.controller.ts+3 −38 modified@@ -1,7 +1,7 @@ import { Controller } from '../common/index.js'; import { PostMessageble } from '../hook/adapter.js'; import { RequestData } from '../common/index.dto.js'; -import { connectService, getClient, clientMap, clientMonitorMap } from './connect.service.js'; +import { connectService, getClient, clientMap, clientMonitorMap, disconnectService } from './connect.service.js'; export class ConnectController { @@ -59,42 +59,7 @@ export class ConnectController { @Controller('disconnect') async disconnect(data: RequestData, webview: PostMessageble) { - const { clientId } = data; - - if (!clientId) { - return { - code: 500, - msg: 'clientId is required' - }; - } - - const client = getClient(clientId); - - if (!client) { - return { - code: 501, - msg: 'mcp client 尚未连接' - }; - } - - try { - // Disconnect the client - client.disconnect(); - - // Remove from maps - clientMap.delete(clientId); - clientMonitorMap.get(clientId)?.close(); - clientMonitorMap.delete(clientId); - - return { - code: 200, - msg: 'Successfully disconnected' - }; - } catch (error) { - return { - code: 500, - msg: `Failed to disconnect: ${error}` - }; - } + const res = await disconnectService(data); + return res; } } \ No newline at end of file
service/src/mcp/connect.service.ts+311 −271 modified@@ -1,5 +1,5 @@ import { exec, execSync, spawnSync } from 'node:child_process'; -import { RequestClientType } from '../common/index.dto.js'; +import { RequestClientType, RequestData } from '../common/index.dto.js'; import { connect } from './client.service.js'; import { RestfulResponse } from '../common/index.dto.js'; import { McpOptions } from './client.dto.js'; @@ -13,38 +13,38 @@ import chalk from 'chalk'; export const clientMap: Map<string, RequestClientType> = new Map(); export function getClient(clientId?: string): RequestClientType | undefined { - return clientMap.get(clientId || ''); + return clientMap.get(clientId || ''); } export const clientMonitorMap: Map<string, McpServerConnectMonitor> = new Map(); export async function updateClientMap(uuid: string, options: McpOptions): Promise<{ res: boolean; error?: any }> { - try { - const client = await connect(options); - clientMap.set(uuid, client); - const tools = await client.listTools(); - console.log( - chalk.white('update client tools'), - chalk.blue(tools.tools.map(tool => tool.name).join(',')) - ); + try { + const client = await connect(options); + clientMap.set(uuid, client); + const tools = await client.listTools(); + console.log( + chalk.white('update client tools'), + chalk.blue(tools.tools.map(tool => tool.name).join(',')) + ); const resourceTemplates = await client.listResourceTemplates(); - console.log( - chalk.white('update client resourceTemplates'), - chalk.blue(resourceTemplates.resourceTemplates.map(r => r.name).join(',')) - ); + console.log( + chalk.white('update client resourceTemplates'), + chalk.blue(resourceTemplates.resourceTemplates.map(r => r.name).join(',')) + ); const resources = await client.listResources(); - console.log( - chalk.white('update client resources'), - chalk.blue(resources.resources.map(r => r.name).join(',')) - ); + console.log( + chalk.white('update client resources'), + chalk.blue(resources.resources.map(r => r.name).join(',')) + ); const prompts = await client.listPrompts(); - console.log( - chalk.white('update client prompts'), - chalk.blue(prompts.prompts.map(p => p.name).join(',')) - ); - return { res: true }; - } catch (error) { - console.error('[updateClientMap] error:', error); - return { res: false, error }; - } + console.log( + chalk.white('update client prompts'), + chalk.blue(prompts.prompts.map(p => p.name).join(',')) + ); + return { res: true }; + } catch (error) { + console.error('[updateClientMap] error:', error); + return { res: false, error }; + } } export function tryGetRunCommandError(command: string, args: string[] = [], cwd?: string): string | null { try { @@ -68,198 +68,198 @@ export function tryGetRunCommandError(command: string, args: string[] = [], cwd? } function getCWD(option: McpOptions) { - // if (option.cwd) { - // return option.cwd; - // } - const file = option.args?.at(-1); - if (file) { - // 如果是绝对路径,直接返回目录 - if (path.isAbsolute(file)) { - // 如果是是文件,则返回文件所在的目录 - if (!fs.existsSync(file)) { - return ''; - } - - if (fs.statSync(file).isDirectory()) { - return file; - } else { - return path.dirname(file); - } - } else { - // 如果是相对路径,根据 cwd 获取真实路径 - const absPath = path.resolve(option.cwd || process.cwd(), file); - - if (!fs.existsSync(absPath)) { - return ''; - } - - // 如果是是文件,则返回文件所在的目录 - if (fs.statSync(absPath).isDirectory()) { - return absPath; - } else { - return path.dirname(absPath); - } - } - } - return undefined; + // if (option.cwd) { + // return option.cwd; + // } + const file = option.args?.at(-1); + if (file) { + // 如果是绝对路径,直接返回目录 + if (path.isAbsolute(file)) { + // 如果是是文件,则返回文件所在的目录 + if (!fs.existsSync(file)) { + return ''; + } + + if (fs.statSync(file).isDirectory()) { + return file; + } else { + return path.dirname(file); + } + } else { + // 如果是相对路径,根据 cwd 获取真实路径 + const absPath = path.resolve(option.cwd || process.cwd(), file); + + if (!fs.existsSync(absPath)) { + return ''; + } + + // 如果是是文件,则返回文件所在的目录 + if (fs.statSync(absPath).isDirectory()) { + return absPath; + } else { + return path.dirname(absPath); + } + } + } + return undefined; } function getCommandFileExt(option: McpOptions) { - const file = option.args?.at(-1); - if (file) { - return path.extname(file); - } - return undefined; + const file = option.args?.at(-1); + if (file) { + return path.extname(file); + } + return undefined; } function collectAllOutputExec(command: string, cwd: string) { - return new Promise<string>((resolve, reject) => { - const handler = setTimeout(() => { - resolve(''); - }, 5000); - - exec(command, { cwd }, (error, stdout, stderr) => { - const errorString = error || ''; - const stdoutString = stdout || ''; - const stderrString = stderr || ''; - - console.log('[collectAllOutputExec]', errorString); - console.log('[collectAllOutputExec]', stdoutString); - console.log('[collectAllOutputExec]', stderrString); - - clearTimeout(handler); - resolve(errorString + stdoutString + stderrString); - }); - }); + return new Promise<string>((resolve, reject) => { + const handler = setTimeout(() => { + resolve(''); + }, 5000); + + exec(command, { cwd }, (error, stdout, stderr) => { + const errorString = error || ''; + const stdoutString = stdout || ''; + const stderrString = stderr || ''; + + console.log('[collectAllOutputExec]', errorString); + console.log('[collectAllOutputExec]', stdoutString); + console.log('[collectAllOutputExec]', stderrString); + + clearTimeout(handler); + resolve(errorString + stdoutString + stderrString); + }); + }); } async function preprocessCommand(option: McpOptions, webview?: PostMessageble) { - // 对于特殊表示的路径,进行特殊的支持 - if (option.args) { - option.args = option.args.map(arg => { - if (arg.startsWith('~/')) { - return arg.replace('~', process.env.HOME || ''); - } - return arg; - }); - } - - if (option.cwd && option.cwd.startsWith('~/')) { - option.cwd = option.cwd.replace('~', process.env.HOME || ''); - } - - if (option.connectionType === 'SSE' || option.connectionType === 'STREAMABLE_HTTP') { - return; - } - - const cwd = getCWD(option); - if (!cwd) { - return; - } - - const ext = getCommandFileExt(option); - if (!ext) { - return; - } - - // STDIO 模式下,对不同类型的项目进行额外支持 - // uv:如果没有初始化,则进行 uv sync,将 mcp 设置为虚拟环境的 - // npm:如果没有初始化,则进行 npm init,将 mcp 设置为虚拟环境 - // go:如果没有初始化,则进行 go mod init - - switch (ext) { - case '.py': - await initUv(option, cwd, webview); - break; - case '.js': - case '.ts': - await initNpm(option, cwd, webview); - break; - - default: - break; - } + // 对于特殊表示的路径,进行特殊的支持 + if (option.args) { + option.args = option.args.map(arg => { + if (arg.startsWith('~/')) { + return arg.replace('~', process.env.HOME || ''); + } + return arg; + }); + } + + if (option.cwd && option.cwd.startsWith('~/')) { + option.cwd = option.cwd.replace('~', process.env.HOME || ''); + } + + if (option.connectionType === 'SSE' || option.connectionType === 'STREAMABLE_HTTP') { + return; + } + + const cwd = getCWD(option); + if (!cwd) { + return; + } + + const ext = getCommandFileExt(option); + if (!ext) { + return; + } + + // STDIO 模式下,对不同类型的项目进行额外支持 + // uv:如果没有初始化,则进行 uv sync,将 mcp 设置为虚拟环境的 + // npm:如果没有初始化,则进行 npm init,将 mcp 设置为虚拟环境 + // go:如果没有初始化,则进行 go mod init + + switch (ext) { + case '.py': + await initUv(option, cwd, webview); + break; + case '.js': + case '.ts': + await initNpm(option, cwd, webview); + break; + + default: + break; + } } async function initUv(option: McpOptions, cwd: string, webview?: PostMessageble) { - let projectDir = cwd; - - while (projectDir!== path.dirname(projectDir)) { - if (fs.readdirSync(projectDir).includes('pyproject.toml')) { - break; - } - projectDir = path.dirname(projectDir); - } - - const venv = path.join(projectDir, '.venv'); - - // judge by OS - const mcpCli = os.platform() === 'win32' ? - path.join(venv, 'Scripts','mcp.exe') : - path.join(venv, 'bin', 'mcp'); - - if (option.command === 'mcp') { - option.command = mcpCli; - // option.cwd = projectDir; - } - - if (fs.existsSync(mcpCli)) { - return ''; - } - - const syncOutput = await collectAllOutputExec('uv sync', projectDir); - - webview?.postMessage({ - command: 'connect/log', - data: { - code: syncOutput.toLowerCase().startsWith('error') ? 501: 200, - msg: { - title: 'uv sync', - message: syncOutput - } - } - }); - - const addOutput = await collectAllOutputExec('uv add mcp "mcp[cli]"', projectDir); - webview?.postMessage({ - command: 'connect/log', - data: { - code: addOutput.toLowerCase().startsWith('error') ? 501: 200, - msg: { - title: 'uv add mcp "mcp[cli]"', - message: addOutput - } - } - }); + let projectDir = cwd; + + while (projectDir !== path.dirname(projectDir)) { + if (fs.readdirSync(projectDir).includes('pyproject.toml')) { + break; + } + projectDir = path.dirname(projectDir); + } + + const venv = path.join(projectDir, '.venv'); + + // judge by OS + const mcpCli = os.platform() === 'win32' ? + path.join(venv, 'Scripts', 'mcp.exe') : + path.join(venv, 'bin', 'mcp'); + + if (option.command === 'mcp') { + option.command = mcpCli; + // option.cwd = projectDir; + } + + if (fs.existsSync(mcpCli)) { + return ''; + } + + const syncOutput = await collectAllOutputExec('uv sync', projectDir); + + webview?.postMessage({ + command: 'connect/log', + data: { + code: syncOutput.toLowerCase().startsWith('error') ? 501 : 200, + msg: { + title: 'uv sync', + message: syncOutput + } + } + }); + + const addOutput = await collectAllOutputExec('uv add mcp "mcp[cli]"', projectDir); + webview?.postMessage({ + command: 'connect/log', + data: { + code: addOutput.toLowerCase().startsWith('error') ? 501 : 200, + msg: { + title: 'uv add mcp "mcp[cli]"', + message: addOutput + } + } + }); } async function initNpm(option: McpOptions, cwd: string, webview?: PostMessageble) { - let projectDir = cwd; - - while (projectDir !== path.dirname(projectDir)) { - if (fs.readdirSync(projectDir).includes('package.json')) { - break; - } - projectDir = path.dirname(projectDir); - } - - const nodeModulesPath = path.join(projectDir, 'node_modules'); - if (fs.existsSync(nodeModulesPath)) { - return ''; - } - - const installOutput = execSync('npm i', { cwd: projectDir }).toString('utf-8') + '\n'; - webview?.postMessage({ - command: 'connect/log', - data: { - code: installOutput.toLowerCase().startsWith('error')? 200: 501, - msg: { - title: 'npm i', - message: installOutput - } - } - }) + let projectDir = cwd; + + while (projectDir !== path.dirname(projectDir)) { + if (fs.readdirSync(projectDir).includes('package.json')) { + break; + } + projectDir = path.dirname(projectDir); + } + + const nodeModulesPath = path.join(projectDir, 'node_modules'); + if (fs.existsSync(nodeModulesPath)) { + return ''; + } + + const installOutput = execSync('npm i', { cwd: projectDir }).toString('utf-8') + '\n'; + webview?.postMessage({ + command: 'connect/log', + data: { + code: installOutput.toLowerCase().startsWith('error') ? 200 : 501, + msg: { + title: 'npm i', + message: installOutput + } + } + }) } @@ -269,7 +269,7 @@ async function deterministicUUID(input: string) { const hashBuffer = await crypto.subtle.digest('SHA-1', msgBuffer); const hashArray = Array.from(new Uint8Array(hashBuffer)); const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); - + // 格式化为UUID (版本5) return [ hashHex.substring(0, 8), @@ -282,72 +282,112 @@ async function deterministicUUID(input: string) { export async function connectService( - option: McpOptions, - webview?: PostMessageble + option: McpOptions, + webview?: PostMessageble ): Promise<RestfulResponse> { - try { - - // 预处理字符串 - await preprocessCommand(option, webview); - - // 通过 option 字符串进行 hash,得到唯一的 uuid - const uuid = await deterministicUUID(JSON.stringify(option)); - - const reuseConntion = clientMap.has(uuid); - - // if (!clientMap.has(uuid)) { - // const client = await connect(option); - // clientMap.set(uuid, client); - // } - // const client = clientMap.get(uuid)!; - - { - clientMap.get(uuid)?.disconnect(); - clientMonitorMap.get(uuid)?.close(); - } - - const client = await connect(option); - clientMap.set(uuid, client); - clientMonitorMap.set(uuid, new McpServerConnectMonitor(uuid, option, updateClientMap, webview)); - - const versionInfo = client.getServerVersion(); - - const connectResult = { - code: 200, - msg: { - status: 'success', - clientId: uuid, - reuseConntion, - name: versionInfo?.name, - version: versionInfo?.version - } - }; - - return connectResult; - } catch (error) { - - console.log('[connectService catch error]', error); - - // TODO: 这边获取到的 error 不够精致,如何才能获取到更加精准的错误 - // 比如 error: Failed to spawn: `server.py` - // Caused by: No such file or directory (os error 2) - - let errorMsg = ''; - - if (option.command) { - errorMsg += await collectAllOutputExec( - option.command + ' ' + (option.args || []).join(' '), - option.cwd || process.cwd() - ) - } - - errorMsg += (error as any).toString(); - - const connectResult = { - code: 500, - msg: errorMsg - }; - - return connectResult; - } + try { + + // 预处理字符串 + await preprocessCommand(option, webview); + + // 通过 option 字符串进行 hash,得到唯一的 uuid + const uuid = await deterministicUUID(JSON.stringify(option)); + + const reuseConntion = clientMap.has(uuid); + + // if (!clientMap.has(uuid)) { + // const client = await connect(option); + // clientMap.set(uuid, client); + // } + // const client = clientMap.get(uuid)!; + + { + clientMap.get(uuid)?.disconnect(); + clientMonitorMap.get(uuid)?.close(); + } + + const client = await connect(option); + clientMap.set(uuid, client); + clientMonitorMap.set(uuid, new McpServerConnectMonitor(uuid, option, updateClientMap, webview)); + + const versionInfo = client.getServerVersion(); + + const connectResult = { + code: 200, + msg: { + status: 'success', + clientId: uuid, + reuseConntion, + name: versionInfo?.name, + version: versionInfo?.version + } + }; + + return connectResult; + } catch (error) { + + console.log('[connectService catch error]', error); + + // TODO: 这边获取到的 error 不够精致,如何才能获取到更加精准的错误 + // 比如 error: Failed to spawn: `server.py` + // Caused by: No such file or directory (os error 2) + + let errorMsg = ''; + + if (option.command) { + errorMsg += await collectAllOutputExec( + option.command + ' ' + (option.args || []).join(' '), + option.cwd || process.cwd() + ) + } + + errorMsg += (error as any).toString(); + + const connectResult = { + code: 500, + msg: errorMsg + }; + + return connectResult; + } +} + +export async function disconnectService(data: RequestData) { + const { clientId } = data; + + if (!clientId) { + return { + code: 500, + msg: 'clientId is required' + }; + } + + const client = getClient(clientId); + + if (!client) { + return { + code: 501, + msg: 'mcp client 尚未连接' + }; + } + + try { + // Disconnect the client + client.disconnect(); + + // Remove from maps + clientMap.delete(clientId); + clientMonitorMap.get(clientId)?.close(); + clientMonitorMap.delete(clientId); + + return { + code: 200, + msg: 'Successfully disconnected' + }; + } catch (error) { + return { + code: 500, + msg: `Failed to disconnect: ${error}` + }; + } } \ No newline at end of file
src/webview/webview.service.ts+28 −4 modified@@ -2,7 +2,7 @@ import * as vscode from 'vscode'; import * as fs from 'fs'; import * as fspath from 'path'; import { ConnectionType, detachMcpOptionAsItem, exportFile, McpOptions, panels, updateInstalledConnectionConfig, updateWorkspaceConnectionConfig } from '../global.js'; -import { routeMessage } from '../../openmcp-sdk/service/index.js'; +import { routeMessage, disconnectService } from '../../openmcp-sdk/service/index.js'; export function getWebviewContent(context: vscode.ExtensionContext, panel: vscode.WebviewPanel): string | undefined { const viewRoot = fspath.join(context.extensionPath, 'openmcp-sdk', 'renderer'); @@ -72,8 +72,10 @@ export function revealOpenMcpWebviewPanel( panel.webview.html = html || ''; panel.iconPath = vscode.Uri.file(fspath.join(context.extensionPath, 'openmcp-sdk', 'renderer', 'images', 'openmcp.png')); + let clientId = ''; + // 处理来自webview的消息 - panel.webview.onDidReceiveMessage(message => { + panel.webview.onDidReceiveMessage(async message => { const { command, data } = message; console.log('receive message', message); @@ -116,7 +118,20 @@ export function revealOpenMcpWebviewPanel( break; default: - routeMessage(command, data, panel.webview); + const res = await routeMessage(command, data, panel.webview); + + if (command === 'connect') { + + if (res?.code !== 200) { + vscode.window.showErrorMessage('Failed to connect to the MCP Server.'); + return; + } + + if (res?.msg?.clientId) { + clientId = res.msg.clientId; + } + } + break; } @@ -126,7 +141,16 @@ export function revealOpenMcpWebviewPanel( // 删除 panels.delete(panelKey); - // TODO: 通过引用计数器关闭后端的 clientMap + vscode.window.showInformationMessage('Disconnected from the MCP Server.'); + + // 关闭后端的连接 + if (clientId) { + const res = await disconnectService({ clientId }); + + if (res.code !== 200) { + vscode.window.showErrorMessage('Fail to disconnect, please disconnect manually'); + } + } // 退出 panel.dispose();
9c3799d6ffaefix(auth): sanitize authorization UR
2 files changed · +4 −2
package.json+2 −1 modified@@ -287,7 +287,8 @@ "tesseract.js": "^6.0.1", "tslib": "^2.8.1", "uuid": "^11.1.0", - "ws": "^8.18.1" + "ws": "^8.18.1", + "strict-url-sanitise": "^0.0.1" }, "devDependencies": { "@rollup/plugin-babel": "^6.0.4",
service/src/mcp/auth.service.ts+2 −1 modified@@ -3,6 +3,7 @@ import { URL } from 'node:url'; import { OAuthClientInformation, OAuthClientInformationFull, OAuthClientMetadata, OAuthTokens } from '@modelcontextprotocol/sdk/shared/auth.js'; import { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js'; import open from 'open'; +import { sanitizeUrl } from 'strict-url-sanitise'; // const CALLBACK_PORT = 16203; // Use different port than auth server (3001) // const CALLBACK_URL = `http://localhost:${CALLBACK_PORT}/callback`; @@ -179,7 +180,7 @@ export class OAuthClient { public async openBrowser(url: string): Promise<void> { console.log(`🌐 Opening browser for authorization: ${url}`); - await open(url); // 自动适配不同操作系统 + await open(sanitizeUrl(url)); // 自动适配不同操作系统 } }
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
3News mentions
0No linked articles in our index yet.