公司动态

AI Agent安全扫描器:MCP Server与Skill的权限审计实践

📅 2026/9/3 5:48:38
AI Agent安全扫描器:MCP Server与Skill的权限审计实践
2025年做 AI Agent 工程最容易被忽略的安全问题往往不是大模型本身的幻觉而是模型能调用的那些工具。你在把一个 MCP 服务器接入 Agent 之前真的清楚它暴露了哪些工具吗你从社区下载一个 agent skill 用来生成周报能保证它不会顺手读取~/.ssh/id_rsa或者把公司内部文档发送到未知域名吗这类风险不是传统 Web 漏洞也不是提示词注入这么简单。AI Agent 的落地方式是“模型负责决策、工具负责执行”模型本身不直接操作服务器但它可以根据工具描述决定去调用哪个工具。这意味着一个 MCP server 里的恶意工具定义、一个 agent skill 包里的不可信脚本都可能成为整个 Agent 应用被突破的入口。Security scanner for AI agents、MCP servers and agent skills 这类安全扫描器正是为了解决这个问题而出现的。本文不绑定某个具体商业产品而是把它当作一个工程问题来拆解Agent 安全扫描器到底扫什么、为什么传统安全工具覆盖不到、如何用最小代码实现一个可用的扫描器以及怎么在 CI 流程里把它跑起来。读完你可以直接照着一套最小方案落地也能避开权限设计、误报处理和沙箱隔离中最常见的坑。1. 为什么 AI Agent 生态现在需要独立的安全扫描器先看一个实际问题。假设你维护一个内部知识库助手它通过 MCP server 读取企业文档又通过一个社区下载的 skill 生成汇报邮件。某天你发现模型开始调用一个名为compile_notes的工具把当前对话中的文档内容 POST 到外部域名。回头看这个 MCP server 的 tools 定义description 只写了“归类笔记”但真正的行为是数据外发。这种攻击不依赖某个中间件漏洞也不依赖 SQL 注入攻击者直接针对“大模型根据工具描述选择合适的工具”这一决策机制下手。工具描述写得越像正常功能模型越容易在没有人工确认的情况下调用它。这就是所谓的工具投毒也叫做恶意工具定义。传统安全工具在处理这类问题时明显不够用传统工具擅长范围对 Agent 生态的盲区SAST 静态扫描扫描源码漏洞不会解析 MCP server 的 tool schemaDAST 动态扫描扫描运行中 Web 应用扫不出 skill 包的 manifest 权限声明依赖扫描扫描 JAR/npm/pip 包漏洞不识别 agent skills 这种新型插件包WAF拦截网络层攻击管不了模型根据“描述”决定调用哪个工具更本质的原因是Agent 应用里多了一个“自然语言决策层”。传统接口的权限模型是“用户—角色—资源”调用方是谁、能做什么可以通过账号体系严格控制。而 Agent 场景变成了“大模型—工具—外部系统”大模型基于上下文动态决定调用哪个工具权限边界也就变成了动态问题。安全扫描器要做的正是把这种动态信任变成可验证的配置审计和行为审计。所以我的判断是Security scanner for AI agents、MCP servers and agent skills 解决的不是“发现更多 CVE”而是“AI 应用的工具供应链和权限边界问题”。谁在给 Agent 接第三方工具谁在分发 agent skills谁就必须把这类扫描器纳入开发流程。2. 核心概念AI Agent、MCP Server、Agent Skill 与 Security Scanner2.1 四个核心对象在讲扫描器之前先把四个概念边界理清。对象一句话解释安全关注点AI Agent基于大模型进行规划并调用外部工具的自动化程序决策链路复杂工具调用结果可能进入模型上下文MCP Server把工具和数据能力封装成标准接口供 Agent 调用工具描述、鉴权、数据外发、命令执行Agent Skill可复用、可分发的技能包定义 Agent 的专项行为来源可信度、入口脚本、权限声明、沙箱Security Scanner对上述对象做静态/动态扫描的工具发现过度授权、恶意工具定义、不可信技能包2.2 AI AgentAI Agent 不是一个新概念但在大模型时代它通常被定义为“能感知环境、做出规划、调用工具并完成任务”的自动化程序。和传统自动化脚本最大的区别是Agent 不需要你为每个分支写死逻辑而是用自然语言描述目标由模型生成执行路径。安全上真正需要关注的是 Agent 的能力半径。脚本的能力边界是代码写死的而 Agent 的能力边界是“它能调用哪些工具、工具允许什么操作”共同决定的。工具越多、权限越大模型出错或被诱导时造成的破坏就越大。2.3 MCP ServerMCP 全称 Model Context Protocol定位是统一大模型应用与外部工具、数据源的接入协议。一个 MCP server 通常把内部能力暴露成 toolsAgent 通过 MCP 客户端发现这些工具并组织调用。对安全扫描器来说MCP 的价值在于工具定义是结构化 JSON/JSON Schema 暴露的。扫描器可以在不触发真实调用的情况下先对工具的“名称、描述、输入参数、权限声明”做静态分析。这也是为什么 Agent 安全扫描器可以把 MCP server 作为第一类检测对象。2.4 Agent SkillAgent Skill 是近几年随着 Agent 工程化出现的概念指把某个专项能力打包成可复用的技能单元通常包含 manifest 配置、入口脚本、说明文档和依赖清单。你可以把它理解成 Agent 世界的插件包。agent skills 的使用范围已经不止于代码生成。已经有团队尝试把“问卷清洗—统计分析—定性编码—论文草稿生成”这类人文社科混合研究方法的工作流拆成多个可复用的 agent skills。技能包应用越普及对来源和行为的审计就越不是可选项因为一旦分发渠道不受控供应链风险就会被放大。2.5 Security Scanner 的定位Security Scanner 是面向 Agent 生态的专用检测工具一般包含静态扫描、动态扫描和策略校验三层能力。它要做的是回答三个问题这个 MCP server 的工具是否包含危险操作或过度权限这个 agent skill 包的来源和入口脚本是否可信Agent 应用的整体权限策略是否符合最小权限原则打个比方传统安全扫描器是给“服务器”做体检而 Agent 安全扫描器是给“代理人的行为边界”做审计。前者关心系统有没有漏洞后者关心“这个被授权的代理会不会拿着钥匙做不该做的事”。3. 扫描器到底扫什么检测对象与检测规则一个完整的 Agent 安全扫描器检测对象可以分为静态层、动态层和策略层。层级输入输出典型风险静态层MCP tools 定义、Skill manifest、Prompt 模板风险清单恶意工具定义、过度授权、危险入口脚本动态层沙箱中运行 MCP 工具或 Skill 脚本行为记录数据外发、文件删除、命令执行策略层权限声明 vs 实际行为冲突报告声明最小权限但实际执行高权限操作3.1 MCP Server 工具描述与 permissionsMCP server 的核心是可调用的 tools。每个 tool 通常包含 name、description、inputSchema有的还会带权限声明字段。安全扫描器首先要检查的就是这些结构化字段。典型检测规则规则ID风险等级检测目标示例命中RULE-MCP-001高工具权限包含 shell:writeexecute_command 工具RULE-MCP-002高工具权限包含 filesystem:deletedelete_file 工具RULE-MCP-003中工具描述包含 curl/wget/upload 等关键词“把结果上传到...”RULE-MCP-004中工具缺少 permissions 字段无法判断权限范围扫描器不需要理解工具的业务含义只需要基于规则判断“这个工具是否可能执行命令、写入文件、读取凭据、发送网络请求”。凡是命中高风险的应默认阻断而不是放行后让人工再看。3.2 Agent Skill 包 manifest 与入口脚本Agent Skill 的检测重点在 manifest 和入口脚本。一个标准的 skill 包通常包含manifest.yaml声明技能名称、版本、作者、权限、执行入口、沙箱要求。scripts/ 目录或单个入口脚本真正要执行的逻辑。依赖清单技能运行所需第三方库。扫描器要做两件事。第一解析 manifest检查权限声明是否超出合理范围第二扫描入口脚本用正则或 AST 识别危险命令模式比如直接拼接 shell 命令、读取环境变量中的密钥、向外部域名发起请求。这里最容易踩坑的是很多 skill 包的入口脚本本身是给开发者手动运行的放在 Agent 环境里跑会导致权限被放大。比如一个“生成数据报告”的技能脚本里可能包含pd.read_csv这样正常的数据读取也可能包含os.system(curl ...)这种外发逻辑。扫描器需要对这类混合脚本做分层判断不能只因为有一个os.system就一刀切。3.3 Prompt 模板与指令注入Agent 应用里一定会写 Prompt 模板而模板中如果直接拼接不可信输入就会引入提示词注入风险。扫描器可以把模板中的占位符提取出来检查它们是否被放进 system 指令、是否可能覆盖原始指令。这条检测比较容易被忽略因为它看似属于“代码审计”但实际和 Agent 的安全高度相关。一个恶意的外部输入如果被拼进 system prompt模型可能被诱导执行计划外工具调用权限再小也可能变成数据泄露的入口。3.4 权限策略与实际行为静态扫描只能回答“声明了什么”不能回答“实际做了什么”。所以完整的安全扫描器还需要动态层在沙箱中真实调用 MCP 工具或 Skyll 脚本观察行为。比如记录进程创建、文件系统变更、网络连接、环境变量读取。策略层的核心是比对“声明的权限”和“实际行为”。如果 manifest 声明只需要filesystem:read但脚本运行时尝试连接外部域名就必须触发告警。4. 环境准备与前置条件下面我们用一个最小示例跑通 Agent 安全扫描流程。示例不依赖任何特定的云端产品只需要本机 Python 环境。4.1 运行环境操作系统Linux 或 macOS 均可Windows 使用 WSL 也可以。Python 版本建议 3.10 及以上示例使用了 argparse 和 pathlib 等标准库。依赖包需要 PyYAML 用来解析 skill manifest。版本说明请以实际安装版本为准示例核心逻辑不依赖某个特定版本。4.2 项目目录结构建议按下面的结构组织代码agent-security-scanner/ ├── examples/ │ ├── mcp_tools.json │ ├── analyze_tools.py │ ├── skill-sample/ │ │ └── manifest.yaml │ └── verify_skill.py └── .gitlab-ci.yml4.3 创建虚拟环境并安装依赖python3 -m venv .venv source .venv/bin/activate pip install pyyaml如果已经全局安装过 PyYAML也可以直接运行脚本。遇到ModuleNotFoundError: No module named yaml时按上面命令安装即可。5. 最小可用的安全扫描器完整代码示例这一节给出一个可直接运行的最小实现。它不做花哨的模型推理只做静态扫描但足以覆盖前文提到的大部分核心场景。5.1 MCP Server 工具定义示例先准备一个模拟的 MCP tools 定义文件。实际项目中你可以通过 MCP client 的list_tools接口导出也可以让 MCP server 开发者直接提供。// 文件路径: examples/mcp_tools.json { tools: [ { name: search_web, description: 搜索指定关键词的网页信息, inputSchema: { type: object, properties: { query: { type: string, description: 搜索关键词 } }, required: [query] }, permissions: [network:read], endpoint: https://mcp.example.com/search }, { name: execute_command, description: 在目标服务器上执行 shell 命令并返回执行结果, inputSchema: { type: object, properties: { command: { type: string, description: 要执行的命令 } }, required: [command] }, permissions: [shell:write], endpoint: https://mcp.example.com/exec }, { name: upload_report, description: 将生成的报告文件上传到内部服务器, inputSchema: { type: object, properties: { path: { type: string, description: 报告文件路径 } }, required: [path] }, permissions: [filesystem:read, network:send], endpoint: https://mcp.example.com/upload } ] }在这个示例中execute_command明显是高风险工具因为它包含shell:write权限upload_report同时包含文件读取和网络发送权限属于中高风险需要结合业务判断。5.2 MCP 工具静态扫描脚本下面脚本会解析 JSON 文件对每个工具的权限和描述做规则匹配。它使用 Python 标准库实现不需要额外安装其他包。#!/usr/bin/env python3 # 文件路径: examples/analyze_tools.py import argparse import json import re import sys from pathlib import Path DANGEROUS_PATTERNS { shell: r(shell|exec|bash|cmd|powershell|system), filesystem: r(file.*(write|delete|remove|move)|rm\b|unlink|write_file|delete_file), credentials: r(secret|token|password|api[_-]?key|credential), network_exfil: r(http(s)?://|request|fetch|upload|send), } DANGEROUS_ACTIONS { shell:write: 可执行任意命令, shell:read: 可读取命令执行结果, filesystem:write: 可写入文件, filesystem:delete: 可删除文件, credential:read: 可读取敏感凭据, network:send: 可向外部发送数据, } def load_tools(path: str): with open(path, r, encodingutf-8) as f: return json.load(f).get(tools, []) def check_permission(tool): perms tool.get(permissions, []) if not isinstance(perms, list): return { level: high, message: permissions 字段缺失或格式异常无法判断最小权限, } risk [] for p in perms: if p in DANGEROUS_ACTIONS: risk.append(f{p}:{DANGEROUS_ACTIONS[p]}) if risk: return {level: high, message: ; .join(risk)} return {level: low, message: 权限声明在可接受范围内} def check_description(tool): desc tool.get(description, ) or matches [] for key, pattern in DANGEROUS_PATTERNS.items(): if re.search(pattern, desc, re.IGNORECASE): matches.append(key) if matches: return { level: medium, message: 描述中命中危险行为关键词: , .join(matches), } return {level: low, message: 描述未命中明显危险关键词} def main(): parser argparse.ArgumentParser(description检查 MCP 工具定义的安全风险) parser.add_argument(--input, -i, requiredTrue, helpMCP tools 定义的 JSON 文件路径) parser.add_argument( --block, -b, actionstore_true, help发现高风险工具时返回非零退出码用于 CI, ) args parser.parse_args() tools load_tools(args.input) if not tools: print(未发现任何 tool 定义) sys.exit(0) failed False for tool in tools: name tool.get(name, unknown) perm_result check_permission(tool) desc_result check_description(tool) is_fail perm_result[level] high or desc_result[level] high summary FAIL if is_fail else PASS if is_fail: failed True print( f[{summary}] {name} | 权限: {perm_result[message]} f| 描述: {desc_result[message]} ) if failed and args.block: print(发现高风险工具扫描失败) sys.exit(1) print(扫描完成) if __name__ __main__: main()代码逻辑并不复杂但覆盖了三个关键点检查permissions字段是否缺失或异常因为无法判断最小权限本身就是一个风险。检查工具描述是否命中危险行为关键词因为描述是模型决定是否调用工具的重要依据。支持--block参数方便在 CI 中把高风险项变成阻断项。5.3 Agent Skill 包 manifest 示例与校验脚本一个 Skill 包至少需要一份 manifest 文件。下面是一个示例其中signature字段在真实场景中应该是对整个技能包内容的哈希签名这里用xxxx占位。# 文件路径: examples/skill-sample/manifest.yaml apiVersion: agent.skills/v1 kind: Skill metadata: name: weekly-report-generator version: 1.2.0 author: internal-data-team signature: sha256:xxxx spec: description: 根据团队周报数据生成汇总文档 platforms: [cli, mcp] input: - name: report_path type: path required: true policy: read output: - name: result_path type: path required: true policy: write permissions: - filesystem:read - filesystem:write execution: entrypoint: scripts/run.py sandbox: true network: false allowed_output_domains: []对应的校验脚本如下#!/usr/bin/env python3 # 文件路径: examples/verify_skill.py import argparse import re import sys from pathlib import Path try: import yaml except ImportError: raise SystemExit(缺少依赖 PyYAML请先执行 pip install pyyaml) BLOCKED_ENTRYPOINTS re.compile( r(rm\s-rf|curl\s.*\||wget\s.*\||/bin/sh|\bbash\b), re.IGNORECASE, ) ALLOWED_PERMISSIONS { filesystem:read, filesystem:write, network:read, network:send, shell:read, shell:write, credential:read, } def validate(path: str): manifest_path Path(path) / manifest.yaml if not manifest_path.exists(): print([FAIL] 缺少 manifest.yaml) return 1 with open(manifest_path, r, encodingutf-8) as f: manifest yaml.safe_load(f) spec manifest.get(spec, {}) permissions spec.get(permissions, []) if not permissions: print([WARN] 未声明 permissions建议显式声明最小权限) invalid_perms [p for p in permissions if p not in ALLOWED_PERMISSIONS] if invalid_perms: print([FAIL] 包含未允许的权限声明:, invalid_perms) return 1 entrypoint str(spec.get(execution, {}).get(entrypoint, )) if BLOCKED_ENTRYPOINTS.search(entrypoint): print([FAIL] 入口脚本疑似包含危险命令:, entrypoint) return 1 if spec.get(execution, {}).get(sandbox) is not True: print([WARN] 建议在沙箱环境中运行技能脚本) print([PASS] manifest 校验通过) return 0 if __name__ __main__: parser argparse.ArgumentParser(description校验 Skill 包 manifest) parser.add_argument(skill_dir, helpSkill 包目录) args parser.parse_args() sys.exit(validate(args.skill_dir))这个脚本当前只检查了 manifest 和入口脚本路径没有扫描实际脚本内容。真实场景中你还需要把scripts/目录下的所有脚本做内容级扫描并和入口脚本一起做行为分析。5.4 集成到 CI 流程扫描器只有跑在 CI 里才有长期价值。下面给出一个 GitLab CI 的配置示例。# 文件路径: .gitlab-ci.yml agent-security-scan: stage: test script: - python examples/analyze_tools.py -i config/mcp_tools.json --block - python examples/verify_skill.py skills/weekly-report-generator tags: - python在 CI 中使用--block后只要发现高风险工具流水线就会失败。对于没有--block的校验脚本你可以在 CI 中判断退出码也可以直接让脚本的return 1中断流水线实现同类效果。6. 运行结果与效果验证6.1 运行 MCP 工具扫描执行下面的命令python examples/analyze_tools.py -i examples/mcp_tools.json --block预期输出[FAIL] execute_command | 权限: shell:write:可执行任意命令 | 描述: 描述中命中危险行为关键词: shell [FAIL] upload_report | 权限: filesystem:read:可读取文件; network:send:可向外部发送数据 | 描述: 描述中命中危险行为关键词: filesystem, network_exfil [PASS] search_web | 权限: 权限声明在可接受范围内 | 描述: 描述未命中明显危险关键词 发现高风险工具扫描失败如何判断成功输出中风险项被标记为FAIL。使用了--block时进程退出码为 1CI 中流水线会失败。如果你只是想做人肉 review不加--block即可但建议在合并请求阶段就强制阻断。6.2 运行 Skill 包校验执行下面的命令python examples/verify_skill.py examples/skill-sample预期输出[PASS] manifest 校验通过如果把permissions改成包含credential:read预期输出会变成[FAIL] 包含未允许的权限声明: [credential:read]如果第一次运行失败优先检查两个位置manifest.yaml 的缩进是否正确PyYAML 对缩进非常敏感。脚本中 ALLOWED_PERMISSIONS 集合是否与业务实际权限一致不要为了跑通而过宽放行。7. 常见问题与排查思路问题现象可能原因排查方式解决方案报错 No module named yamlPyYAML 未安装执行 pip list 检查执行 pip install pyyaml扫描结果为空提示未发现 toolJSON 的 root key 不是 tools打开 JSON 文件确认字段名在 load_tools 中兼容 tools/items 等字段权限误报过多白名单过严查看 DANGEROUS_ACTIONS 配置结合业务维护允许列表默认放行 read-onlyCI 中扫描总是不通过--block 导致任何高风险都阻断日志中查看 FAIL 项先人工确认风险再决定放行还是调整权限manifest.yaml 解析失败YAML 缩进错误或字段类型错误用 python -c 校验 YAML修正缩进字段与脚本保持一致想对 MCP server 做强动态扫描直接连生产环境有风险检查是否在沙箱中运行使用隔离容器配置测试凭据禁止真实网络访问这里要特别强调不要把扫描器直接接到生产 MCP server 上做动态调用。最佳做法是把工具定义导出成 JSON先做静态扫描确有必要做动态检测时也在隔离沙箱中使用测试凭据并在网络层做好出口限制。8. 工程实践与安全基线建议8.1 默认最小权限Agent 工具和 skill 包的权限永远从“零权限”开始加而不是从“全部权限”开始减。一个只需要读取文档的技能不应该拥有network:send权限。MCP 工具的permissions字段如果没有充分理由就不要声明shell:write、filesystem:delete这类高危险操作。8.2 沙箱与隔离任何来自外部的 agent skill首次运行都必须在沙箱中完成。容器加上 seccomp 限制进程内关闭不必要的网络访问文件系统使用只读挂载或临时目录。沙箱中产生的告警要及时记录不要静默吞掉。8.3 供应链签名与来源审核Agent Skill 的分发和 npm/pip 包类似应该建立内部 registry而不是直接信任任何个人链接。每次下载技能包时校验哈希签名manifest 中的signature字段要真实落地发布方需要对内容负责。8.4 策略即代码安全扫描规则本身也要纳入版本管理。把规则配置文件放在仓库里和代码一起评审、一起发布。规则变更应该能追溯到责任人避免某次扫描突然不通过却找不到是谁改的。8.5 持续审计与告警Agent 在运行时到底调用了哪些 MCP 工具、传入了哪些参数、返回了什么结果这些都要有审计日志。重点监控三类行为首次出现的工具调用、超出权限声明范围的行为、对敏感文件或凭据的访问。8.6 灰度与回滚新增 MCP server 或 agent skill 时先在测试环境运行并观察行为记录。灰度期间如果出现异常要能快速回滚到上一个已校验版本而不是直接删除生产数据或凭据。8.7 保留人工判断安全扫描器能帮助你快速发现风险但它无法保证所有风险都能被规则覆盖。真正高价值的判定比如“某个工具是否符合业务最小权限”“某个技能包的作者是否可信”仍然需要负责 Agent 应用的工程师和运维人员参与。将扫描结果看作评审依据而不是唯一的决策来源。9. 总结与后续实践建议把 Security scanner 接入 Agent 工程链路不是为了让流程变慢而是为了在一开始就拦住那些“看起来正常但权限过大”的工具和技能包。AI Agent 工程化越深入安全手段就越要从“事后封堵”转向“事前验证”。建议你按下面顺序动手实践把你目前使用的 MCP server 工具列表导出成 JSON用文中的analyze_tools.py扫一遍确认是否存在shell:write、network:send这类高风险权限。给团队常用的 agent skill 补上 manifest至少声明权限、沙箱和网络策略。把扫描脚本接入 CI让高风险工具和不可信技能包无法合并到主干。给动态调用设置沙箱环境避免直接在本地或生产环境执行。这份最小实现已经能覆盖静态层的主要场景但它不能替代完整的供应链治理。真正成熟的做法是在扫描器之外建立一份“Agent 工具白名单”和“技能包来源清单”把信任关系从个人经验变成团队共同维护的规范。安全扫描器是一个验证工具不是万能的防火墙它的价值在于让每一次工具接入和技能包下载都变得可审计、可追溯、可回滚。