AI Agent安全扫描器:MCP Server与Skill的权限审计实践
2026/9/16 12:13:50 网站建设 项目流程

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 schema
DAST 动态扫描扫描运行中 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 Scanner

2.1 四个核心对象

在讲扫描器之前,先把四个概念边界理清。

对象一句话解释安全关注点
AI Agent基于大模型进行规划并调用外部工具的自动化程序决策链路复杂,工具调用结果可能进入模型上下文
MCP Server把工具和数据能力封装成标准接口供 Agent 调用工具描述、鉴权、数据外发、命令执行
Agent Skill可复用、可分发的技能包,定义 Agent 的专项行为来源可信度、入口脚本、权限声明、沙箱
Security Scanner对上述对象做静态/动态扫描的工具发现过度授权、恶意工具定义、不可信技能包

2.2 AI Agent

AI Agent 不是一个新概念,但在大模型时代,它通常被定义为“能感知环境、做出规划、调用工具并完成任务”的自动化程序。和传统自动化脚本最大的区别是,Agent 不需要你为每个分支写死逻辑,而是用自然语言描述目标,由模型生成执行路径。

安全上真正需要关注的,是 Agent 的能力半径。脚本的能力边界是代码写死的,而 Agent 的能力边界是“它能调用哪些工具、工具允许什么操作”共同决定的。工具越多、权限越大,模型出错或被诱导时造成的破坏就越大。

2.3 MCP Server

MCP 全称 Model Context Protocol,定位是统一大模型应用与外部工具、数据源的接入协议。一个 MCP server 通常把内部能力暴露成 tools,Agent 通过 MCP 客户端发现这些工具并组织调用。

对安全扫描器来说,MCP 的价值在于工具定义是结构化 JSON/JSON Schema 暴露的。扫描器可以在不触发真实调用的情况下,先对工具的“名称、描述、输入参数、权限声明”做静态分析。这也是为什么 Agent 安全扫描器可以把 MCP server 作为第一类检测对象。

2.4 Agent Skill

Agent 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 工具描述与 permissions

MCP 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.yml

4.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", encoding="utf-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", required=True, help="MCP tools 定义的 JSON 文件路径") parser.add_argument( "--block", "-b", action="store_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", encoding="utf-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", help="Skill 包目录") 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时,进程退出码为 1,CI 中流水线会失败。
  • 如果你只是想做人肉 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 'yaml'PyYAML 未安装执行 pip list 检查执行 pip install pyyaml
扫描结果为空,提示未发现 toolJSON 的 root key 不是 tools打开 JSON 文件确认字段名在 load_tools 中兼容 tools/items 等字段
权限误报过多白名单过严查看 DANGEROUS_ACTIONS 配置结合业务维护允许列表,默认放行 read-only
CI 中扫描总是不通过--block 导致任何高风险都阻断日志中查看 FAIL 项先人工确认风险,再决定放行还是调整权限
manifest.yaml 解析失败YAML 缩进错误或字段类型错误用 python -c 校验 YAML修正缩进,字段与脚本保持一致
想对 MCP server 做强动态扫描直接连生产环境有风险检查是否在沙箱中运行使用隔离容器,配置测试凭据,禁止真实网络访问

这里要特别强调:不要把扫描器直接接到生产 MCP server 上做动态调用。最佳做法是把工具定义导出成 JSON,先做静态扫描;确有必要做动态检测时,也在隔离沙箱中使用测试凭据,并在网络层做好出口限制。

8. 工程实践与安全基线建议

8.1 默认最小权限

Agent 工具和 skill 包的权限永远从“零权限”开始加,而不是从“全部权限”开始减。一个只需要读取文档的技能,不应该拥有network:send权限。MCP 工具的permissions字段如果没有充分理由,就不要声明shell:writefilesystem: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 工程化越深入,安全手段就越要从“事后封堵”转向“事前验证”。

建议你按下面顺序动手实践:

  1. 把你目前使用的 MCP server 工具列表导出成 JSON,用文中的analyze_tools.py扫一遍,确认是否存在shell:writenetwork:send这类高风险权限。
  2. 给团队常用的 agent skill 补上 manifest,至少声明权限、沙箱和网络策略。
  3. 把扫描脚本接入 CI,让高风险工具和不可信技能包无法合并到主干。
  4. 给动态调用设置沙箱环境,避免直接在本地或生产环境执行。

这份最小实现已经能覆盖静态层的主要场景,但它不能替代完整的供应链治理。真正成熟的做法是,在扫描器之外建立一份“Agent 工具白名单”和“技能包来源清单”,把信任关系从个人经验变成团队共同维护的规范。安全扫描器是一个验证工具,不是万能的防火墙,它的价值在于让每一次工具接入和技能包下载都变得可审计、可追溯、可回滚。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询