
1. 项目概述从一次内部安全审计说起上个月我们团队在对一个基于MCPModel Context Protocol协议构建的智能体应用进行例行安全审计时发现了一个看似不起眼却影响深远的问题应用将用于访问大模型API的密钥以明文形式直接硬编码在了一个前端JavaScript配置文件里。这个发现让我惊出一身冷汗。MCP协议作为连接智能体与外部工具、数据源的新兴标准其设计初衷是为了实现更灵活、更强大的AI能力编排。然而随着生态的快速扩张许多开发者包括一些经验丰富的同行都忽略了协议栈中一个最基础也最致命的安全环节——凭证的安全存储与传输。这不仅仅是某个应用的疏忽它折射出在追逐AI应用快速上线的热潮中对安全基线的普遍性忽视。今天我就结合这次审计发现以及后续的修复实践深入聊聊MCP协议生态中“不安全凭证存储”这个高频漏洞的成因、危害以及一套可落地、可复用的安全加固方案。无论你是正在构建基于MCP的AI智能体还是在使用相关服务这篇文章都能帮你建立起关键的安全防线。简单来说MCP协议定义了AI模型如ChatGPT、Claude与“服务器”这里指提供特定功能或数据的后端服务之间进行通信的规范。智能体通过MCP服务器获取工具调用、数据查询等能力。而连接MCP服务器往往需要认证凭证API Key、Token、用户名密码等。问题就出在这些凭证的“栖身之所”和“旅行方式”上。不安全的存储就像把家门钥匙藏在脚垫下面攻击者一旦发现就能长驱直入窃取核心资产甚至取得系统控制权。本文将拆解四种典型的不安全存储模式并给出从开发、配置到运维的全链路安全实践。2. MCP协议与凭证存储漏洞深度解析2.1 MCP协议通信模型与安全边界要理解漏洞首先得清楚MCP协议的工作机制。你可以把MCP想象成AI世界的“USB协议”或“插件标准”。核心参与方有三个客户端Client通常是AI模型或智能体应用如集成了MCP客户端的ChatGPT界面。服务器Server提供特定工具或数据访问能力的独立进程例如一个能查询数据库、调用外部API或执行代码的MCP服务。协议Protocol定义Client和Server之间通过标准输入输出stdio或网络进行JSON-RPC通信的格式包括工具列表、调用、结果返回等。在这个模型里凭证Credentials主要用于两个场景一是Client可能需要凭证来认证和调用某个MCP Server如果该Server需要认证二是MCP Server本身在履行其功能时如访问第三方API、连接数据库需要使用凭证。我们讨论的“不安全存储”主要聚焦于这些凭证在Client或Server的代码、配置文件中如何被保存。安全边界变得模糊。传统Web应用中凭证通常保存在后端服务器环境变量或密钥管理器中前端无法触及。但在一些MCP应用架构中为了部署简便开发者可能会将MCP Server的启动配置含凭证与前端智能体代码打包在一起或者直接在客户端配置中写入访问Server的密钥。这就将敏感信息暴露在了不应存在的攻击面上。2.2 四种典型的不安全凭证存储模式根据我们的审计经验和社区反馈以下四种模式最为常见危害也最大。模式一前端源码硬编码这是最危险的低级错误。直接将API密钥、数据库连接字符串等写入前端JavaScript、TypeScript或配置文件如config.js、constants.ts中。// 错误示例config.js export const MCP_SERVER_CONFIG { serverUrl: http://localhost:8080, apiKey: sk-live-abc123def456ghi789jkl012mno345pqr678stu901, // 密钥明文暴露 databaseUrl: postgresql://user:passwordlocalhost:5432/mydb };为什么这是问题前端代码对用户浏览器是透明的可通过开发者工具直接查看。即使经过打包混淆字符串常量也很容易被提取。一旦代码仓库公开如误传到GitHub密钥瞬间泄露。模式二环境配置文件误提交使用.env文件管理环境变量是进步但若将包含真实密钥的.env文件提交到Git版本库风险等同于硬编码。许多.env.example模板文件被填上真实值后一不小心就git add .了。# .env 文件错误提交 OPENAI_API_KEYsk-proj-... MCP_SERVER_TOKENeyJhbGciOiJIUzI1NiIs... AWS_ACCESS_KEY_IDAKIAIOSFODNN7EXAMPLE.env文件本应位于.gitignore中但疏忽难免。攻击者扫描公开Git仓库这是获取高价值凭证的主要途径之一。模式三客户端配置文件无防护在一些桌面端或CLI工具形态的MCP Client中凭证可能被存储在用户目录的明文配置文件如~/.mcp/config.json中。如果文件权限设置不当如全局可读同一台机器的其他用户或恶意软件就能轻易读取。{ servers: { weather_tool: { command: node, args: [./weather-server.js], env: { WEATHER_API_KEY: 12345abcde // 明文存储在本地文件 } } } }模式四日志文件意外输出在调试MCP Server或Client时开发者可能会打印完整的请求/响应日志其中可能包含携带认证头的完整URL或响应体中的敏感数据。这些日志若被输出到控制台、文件或日志收集系统且未脱敏就可能被未授权人员访问。# 控制台日志泄露示例 DEBUG: Calling MCP server with URL: https://api.example.com/tool?tokensecret789 INFO: Database query executed with connection string: postgres://admin:pssw0rdlocalhost/db2.3 漏洞利用场景与潜在影响攻击者利用这些泄露的凭证可以造成多重危害直接经济损失盗用API密钥发起大量计费请求例如滥用OpenAI、Azure OpenAI的额度产生巨额费用。数据泄露通过数据库凭证访问并窃取业务数据、用户隐私信息。权限提升与横向移动利用泄露的云服务密钥如AWS AK/SK在云环境中创建资源、窃取更多数据甚至控制整个云账户。供应链攻击如果泄露的MCP Server被篡改那么所有依赖该Server的AI智能体都可能被投毒执行恶意操作。声誉损失与合规风险数据泄露事件会导致用户信任崩塌并可能违反GDPR、HIPAA等数据保护法规面临法律诉讼和罚款。关键在于这些攻击往往静默发生。攻击者拿到密钥后可以低调地持续窃取数据或资源直到收到天价账单或数据在暗网出现时开发者才后知后觉。3. 安全实践构建MCP应用的凭证安全防线理解了漏洞的形态与危害接下来我们构建从开发到部署的全流程防御体系。安全不是一个功能而是一种贯穿始终的实践。3.1 开发阶段从源头杜绝硬编码原则代码与配置分离配置与密钥分离。实践一使用环境变量并严格管理.gitignore这是最基本也最有效的一步。所有凭证必须通过环境变量注入。# 在启动应用前设置环境变量本地开发 export OPENAI_API_KEYyour-key-here export DATABASE_URLpostgresql://... node your-mcp-server.js在代码中通过process.envNode.js或os.environPython读取。// 正确示例server.js import { config } from dotenv; config(); // 加载 .env 文件到 process.env const serverConfig { apiKey: process.env.OPENAI_API_KEY, // 从环境变量读取 dbUrl: process.env.DATABASE_URL }; // 务必添加验证避免空值导致运行时错误 if (!serverConfig.apiKey) { throw new Error(OPENAI_API_KEY environment variable is required); }关键动作创建.env文件并立即将其加入.gitignore。创建.env.example文件仅包含键名和示例假值并提交此文件作为项目配置文档。# .env.example OPENAI_API_KEYsk-example1234567890 DATABASE_URLpostgresql://user:passwordlocalhost:5432/dbname # .gitignore .env *.env.local .env.*.local实践二为团队和CI/CD配置安全的密钥分发本地开发可以使用.env但在团队协作和自动化部署中需要更安全的方案使用秘密管理工具如1Password、LastPass Teams、Hashicorp Vault等在团队内安全共享密钥。CI/CD平台集成在GitHub Actions、GitLab CI、Jenkins中使用其提供的Secrets功能注入环境变量。绝对不要在Pipeline脚本中明文写入密钥。# GitHub Actions 示例 (部分) jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - name: Deploy to Server run: | ssh userserver export OPENAI_API_KEY${{ secrets.OPENAI_API_KEY }} ./deploy.sh3.2 配置与部署阶段运行时安全原则最小权限动态获取加密存储。实践三使用云厂商或平台的密钥管理服务对于生产环境放弃环境变量文件转而使用专业的密钥管理服务AWSAWS Secrets Manager 或 Parameter Store (SSM)。AzureAzure Key Vault。Google CloudSecret Manager。阿里云密钥管理服务KMS配合凭据管家。华为云数据加密服务DEW。这些服务提供自动轮转、访问审计、细粒度权限控制IAM和加密存储。应用在启动时通过SDK动态获取密钥。// AWS Secrets Manager 示例 (Node.js) import { SecretsManagerClient, GetSecretValueCommand } from aws-sdk/client-secrets-manager; const client new SecretsManagerClient({ region: us-east-1 }); async function getApiKey() { const command new GetSecretValueCommand({ SecretId: prod/MCP/OpenAIKey }); const response await client.send(command); return JSON.parse(response.SecretString).apiKey; // 假设密钥以JSON格式存储 }实践四对静态配置文件进行加密对于必须存在磁盘上的客户端配置文件如桌面应用应对其进行加密。密钥本身由用户在主密码解密后于内存中使用。使用系统提供的安全存储如macOS的Keychain、Windows的Credential Manager、Linux的libsecretGNOME Keyring。使用类似node-keytar这样的库将凭证安全地存储到系统钥匙串。const keytar require(keytar); const SERVICE MyMCPApp; const ACCOUNT userexample.com; // 存储凭证 await keytar.setPassword(SERVICE, ACCOUNT, my-secret-token); // 读取凭证 const token await keytar.getPassword(SERVICE, ACCOUNT);这样配置文件中存储的只是一个标识真正的密钥在系统的安全存储中。3.3 运维与审计阶段持续监控与响应原则假设会被入侵做好检测和止损准备。实践五实施全面的日志脱敏确保应用程序和MCP Server的日志输出中自动过滤掉所有可能的凭证信息。使用日志中间件或包装日志函数对特定模式如/api[Kk]ey([^])/、/token:(\s*)(\S)/进行匹配和替换如替换为[REDACTED]。// 简单的日志脱敏函数示例 function sanitizeLogMessage(message) { const patterns [ [/(api[_-]?key)([^\s])/gi, $1[REDACTED]], [/(token:?\s*)(\S)/gi, $1[REDACTED]], [/(password:?\s*)(\S)/gi, $1[REDACTED]], ]; let sanitized message; patterns.forEach(([regex, replacement]) { sanitized sanitized.replace(regex, replacement); }); return sanitized; } console.log(sanitizeLogMessage(Calling with apikeysk-live-abc123)); // 输出: Calling with apikey[REDACTED]实践六密钥轮转与权限最小化定期轮转为所有API密钥、数据库密码设置有效期并建立定期轮转流程如每90天。使用密钥管理服务可以自动化此过程。最小权限原则为每个MCP Server或应用创建专属的API密钥并赋予其完成工作所必需的最小权限。例如一个仅用于查询的MCP Server其数据库账号应只有SELECT权限没有INSERT、UPDATE、DELETE或DROP权限。在云平台上使用IAM角色和策略精细控制访问范围。实践七建立监控与告警机制监控API使用情况设置费用预算告警和异常用量告警如短时间内调用量激增100倍。监控访问日志分析访问来源IP、时间模式设置对异常地理位置、陌生IP访问的告警。使用专项安全工具集成像GitGuardian这样的工具持续扫描代码仓库包括历史提交是否存在意外提交的密钥并实时告警。4. 实战演练修复一个存在漏洞的MCP Server示例让我们通过一个具体的例子将一个不安全的MCP Server配置改造为安全配置。假设我们有一个提供天气查询功能的MCP Server它需要调用一个外部天气API。4.1 漏洞版本分析// weather-server-insecure.js import { Server } from modelcontextprotocol/sdk/server/index.js; import axios from axios; // 漏洞API密钥硬编码在源码中 const WEATHER_API_KEY 1234567890abcdef; const BASE_URL https://api.weatherapi.com/v1; const server new Server(...); server.setRequestHandler(weather/query, async (request) { const { location } request.params; // 漏洞密钥随请求发送若日志开启会被记录 const response await axios.get(${BASE_URL}/current.json?key${WEATHER_API_KEY}q${location}); return response.data; });这个版本存在两个问题1. 密钥硬编码2. 密钥直接拼接在URL中容易在日志或网络抓包中泄露。4.2 安全加固版本实现// weather-server-secure.js import { Server } from modelcontextprotocol/sdk/server/index.js; import axios from axios; import dotenv from dotenv; import { createHmac } from crypto; // 1. 从环境变量加载配置 dotenv.config(); const WEATHER_API_KEY process.env.WEATHER_API_KEY; const BASE_URL process.env.WEATHER_API_BASE_URL || https://api.weatherapi.com/v1; // 启动时验证关键配置 if (!WEATHER_API_KEY) { console.error(致命错误: WEATHER_API_KEY 环境变量未设置。); process.exit(1); } const server new Server(...); server.setRequestHandler(weather/query, async (request) { const { location } request.params; // 2. 使用axios的params配置避免密钥出现在URL字符串中对某些日志中间件友好 const params new URLSearchParams({ key: WEATHER_API_KEY, q: location, }); try { const response await axios({ method: get, url: ${BASE_URL}/current.json, params: params, // 参数通过axios内部处理 // 3. 可选为外部API请求添加超时和重试逻辑 timeout: 10000, }); // 4. 在返回前可以脱敏响应数据中的任何潜在敏感信息如果存在 const sanitizedData { ...response.data, // 假设原始响应包含内部ID我们将其移除 internalId: undefined, }; delete sanitizedData.internalId; return sanitizedData; } catch (error) { // 5. 错误处理记录错误但避免泄露密钥 console.error(天气查询失败地点: ${location}, error.message); // 抛出一个对用户友好、不暴露内部细节的错误 throw new Error(无法获取 ${location} 的天气信息请稍后重试。); } }); // 6. 使用环境变量控制服务器监听地址和端口 const HOST process.env.HOST || localhost; const PORT parseInt(process.env.PORT || 3000, 10); async function main() { await server.listen({ host: HOST, port: PORT }); console.log(安全版天气MCP Server运行在 ${HOST}:${PORT}); } main();配套的部署与配置创建.env文件并加入.gitignoreWEATHER_API_KEYyour_actual_secret_key_here HOST0.0.0.0 PORT3000使用Docker部署时通过--env-file或Docker Secrets传递环境变量。在Kubernetes中使用Secret对象挂载为环境变量。在服务器上使用systemd服务文件通过EnvironmentFile指令加载受保护的配置文件权限设为600。5. 常见问题排查与进阶安全考量5.1 常见问题速查表问题现象可能原因排查步骤与解决方案应用启动报错“环境变量未定义”1..env文件未加载或路径不对。2. 生产环境未正确配置环境变量。3. 变量名拼写错误。1. 检查dotenv.config()路径或使用dotenv.config({ path: /custom/path/.env })。2. 在服务器上执行printenv密钥似乎已泄露API调用被拒绝1. 密钥确实泄露已被提供商禁用。2. 密钥权限不足或配置错误。3. IP或请求频率被限制。1.立即在API提供商控制台撤销旧密钥生成新密钥并更新所有使用位置。2. 检查密钥关联的权限设置如只读、特定服务。3. 检查是否有异常的请求模式联系提供商支持。从密钥管理服务获取密钥超时1. 网络问题或防火墙规则。2. IAM角色/权限未正确附加给应用实例。3. 密钥管理服务区域配置错误。1. 检查实例的网络连通性telnet或curl测试。2. 验证实例的元数据服务如AWS的IMDS是否可访问IAM角色是否正确。3. 确认SDK中配置的区域与密钥存储的区域一致。日志中仍偶尔出现疑似密钥的字符串1. 脱敏规则不完善未覆盖所有格式。2. 第三方库或中间件打印了原始请求。3. 错误堆栈信息中包含敏感数据。1. 审查和扩充日志脱敏的正则表达式模式。2. 检查并配置第三方中间件如Express的body-parser、HTTP客户端的日志级别。3. 自定义错误处理函数在输出错误前对error.message和error.stack进行脱敏。5.2 进阶安全考量超越存储解决了存储问题安全之路才走完一半。在MCP协议的应用中还需关注传输安全TLS/HTTPS确保MCP Client与Server之间以及Server与外部API之间的所有通信都使用TLS加密HTTPS。避免使用http://localhost进行生产环境通信本地开发也应考虑使用自签名证书或localhost的HTTPS。对于MCP over stdio由于是本地进程间通信风险相对较低但仍需警惕通过stdio传递敏感参数时被其他进程窥探的可能性尽管很难。认证与授权细化不仅要有凭证还要用好凭证。为不同的MCP Server使用不同的、具有最小权限的API密钥。考虑在MCP Server层面实现更细粒度的访问控制例如验证调用方Client的身份令牌JWT或者基于请求内容进行授权。依赖安全定期使用npm audit、pip-audit、cargo audit等工具扫描项目依赖更新存在已知漏洞的包。特别是MCP SDK及其依赖的通信库、解析库。安全开发生命周期SDLC集成将安全扫描SAST、SCA和密钥检测工具集成到代码提交流水线pre-commit hook和CI/CD管道中实现“左移”安全在问题进入仓库或生产环境前就将其阻断。最后安全是一个持续的过程而非一劳永逸的状态。定期回顾和审计你的MCP应用安全状况跟上MCP协议本身的安全更新和最佳实践与社区保持交流才能让你构建的AI智能体在强大功能的同时拥有一副坚实的铠甲。