1
月
在社区成长 1 月
LEVEL 1
1 XP
## 一、背景:为什么 MCP Server 需要安全设计
MCP(Model Context Protocol)是一套让大模型客户端以统一方式发现并调用外部工具、读取资源、使用提示模板的协议。它解决的不是“如何让模型变聪明”,而是“如何把模型与本地文件、数据库、内部 API、代码仓库或业务系统连接起来”。在实际接入中,MCP Server 往往拥有比普通聊天插件更高的权限,例如读取文件、执行查询、访问网络或调用企业内部接口,因此它同时扩大了能力边界和攻击面。
从移动应用安全工程的视角看,MCP Server 可以理解为一个新的本地或远程服务入口:Client 负责发起初始化、能力协商和工具调用,Server 负责暴露工具、资源和提示模板。真正需要重点关注的是工具参数校验、身份认证、最小权限、敏感数据处理、日志审计,以及模型生成内容不可信这一基本前提。本文以本地授权实验为例,演示如何实现一个只读的 MCP Server,并给出客户端接入、权限控制和安全验证方法。
## 二、协议原理与核心对象
MCP 通常基于 JSON-RPC 2.0 进行消息交互。通信双方首先通过初始化流程协商协议版本和能力,随后 Client 可以请求 Server 列出可用工具,Server 返回工具名称、说明和输入参数 Schema。Client 再根据用户意图选择工具并发送调用请求,Server 完成参数验证和业务处理后返回结果。
常见概念包括:
1. Server:提供工具、资源或提示模板的一方。它可以是本地进程,也可以通过 HTTP 等方式部署在远程环境。
2. Client:连接 MCP Server 的宿主程序,例如支持 MCP 的桌面应用、开发工具或自研 Agent。
3. Tool:可被调用的动作,例如查询数据库、读取固定目录文件、检查 APK 元数据。Tool 可能产生副作用,因此必须明确权限和确认机制。
4. Resource:供 Client 读取的上下文资源,例如文档、配置摘要或只读数据。
5. Prompt:可复用的提示模板,用于规范某类任务的输入结构。
6. Transport:消息传输方式。开发阶段常见的是 stdio,远程部署则可能使用基于 HTTP 的传输方式。不同 SDK 的具体接口名称和版本可能存在差异,应以所使用 SDK 的官方文档为准。
## 三、实验目标与前置条件
本文实现一个本地只读 MCP Server,提供两个工具:列出允许目录下的文件,以及读取指定文本文件的前若干字符。它不会执行 Shell 命令,不会访问任意路径,也不会把环境变量、令牌或完整系统信息暴露给模型。
前置条件如下:
- Python 3.10 或更高版本;
- 一个支持 MCP 的本地 Client,或者能够执行 MCP stdio Server 的测试客户端;
- 独立的实验目录,例如 ~/mcp-lab;
- 一个非生产测试账号或本地虚拟环境;
- 不要把真实生产密钥、用户隐私数据和企业源代码放入实验目录。
创建项目目录并初始化虚拟环境:
```bash
mkdir -p ~/mcp-lab/safe-data
cd ~/mcp-lab
python3 -m venv .venv
. .venv/bin/activate
python -m pip install --upgrade pip
```
如果使用的 MCP Python SDK 包名或导入路径与示例不同,应按照当前 SDK 文档调整。依赖安装后,建议执行 `pip freeze` 保存版本,避免因 SDK 升级导致接口行为变化。
## 四、分步骤实现一个最小只读 Server
### 第一步:准备受控数据目录
```bash
printf '这是用于本地测试的公开样例文件。\n' > ~/mcp-lab/safe-data/readme.txt
printf '仅用于验证路径过滤和编码处理。\n' > ~/mcp-lab/safe-data/example.txt
```
不要直接把家目录、项目根目录或系统临时目录作为可读目录。目录边界是安全设计的第一道防线。
### 第二步:编写工具逻辑
下面给出接近常见 Python MCP SDK 风格的示例。不同 SDK 的装饰器、启动方法和返回类型可能不同,重点是数据流和安全校验逻辑,而不是机械复制导入语句:
```python
from pathlib import Path
BASE_DIR = Path.home() / "mcp-lab" / "safe-data"
MAX_BYTES = 4096
def safe_path(name: str) -> Path:
if not isinstance(name, str) or not name.strip():
raise ValueError("name must be a non-empty string")
candidate = (BASE_DIR / name).resolve()
base = BASE_DIR.resolve()
if candidate != base and base not in candidate.parents:
raise ValueError("path is outside the allowed directory")
if candidate.is_symlink():
raise ValueError("symbolic links are not allowed")
return candidate
def list_files() -> list[str]:
result = []
for item in BASE_DIR.iterdir():
if item.is_file() and not item.is_symlink():
result.append(item.name)
return sorted(result)
def read_text(name: str) -> str:
path = safe_path(name)
if not path.is_file():
raise FileNotFoundError("file does not exist")
data = path.read_bytes()
if len(data) > MAX_BYTES:
raise ValueError("file is too large")
return data.decode("utf-8", errors="strict")
```
这里有几个关键点:第一,使用 `resolve()` 处理 `../` 等路径穿越;第二,禁止符号链接,避免链接把访问范围绕到允许目录之外;第三,限制单文件大小,防止大文件拖垮 Client 或上下文窗口;第四,指定 UTF-8 严格解码,避免把不可控二进制内容当成普通文本处理。
### 第三步:注册 MCP 工具
在实际 SDK 中,通常需要创建 Server 实例,并使用工具注册装饰器或显式注册函数:
```python
# 以下为结构示例,具体 import 和启动函数以所用 SDK 版本为准
server = Server("safe-files")
@server.tool()
def list_allowed_files() -> list[str]:
"""列出实验目录中的普通文件,不读取文件内容。"""
return list_files()
@server.tool()
def read_allowed_text(name: str) -> str:
"""读取实验目录内指定 UTF-8 文本文件,最多返回 4096 字节。"""
return read_text(name)
if __name__ == "__main__":
server.run_stdio()
```
工具描述应准确、简短且不能夸大权限。不要写成“读取任意文件”或“执行系统操作”,否则 Client 可能在错误场景下选择该工具。工具返回值也应尽量结构化,例如使用 `{"ok": true, "content": "..."}`,并对错误进行统一处理,避免把堆栈、环境变量或本地路径泄露给调用方。
## 五、在 Client 中接入
以本地 stdio Client 为例,配置中通常需要指定启动命令、工作目录和环境变量。示意配置如下:
```json
{
"mcpServers": {
"safe-files": {
"command": "/home/test/mcp-lab/.venv/bin/python",
"args": ["/home/test/mcp-lab/server.py"],
"env": {
"PYTHONUNBUFFERED": "1"
}
}
}
}
```
路径必须使用实际绝对路径,并尽量使用专门的低权限操作系统账号运行。不要在配置文件中直接写入 API Key、数据库密码或长期 Token。若 Client 支持工具审批,应开启“每次调用前确认”,特别是存在写入、网络请求、代码执行或支付操作的工具。对于只读工具,也建议在初期保留调用确认和详细日志,确认行为稳定后再按风险降低交互频率。
## 六、权限与安全设计清单
1. 最小权限:Server 进程只授予完成任务所需的目录、网络和数据库权限。读取文件的 Server 不应拥有写文件和执行命令权限。
2. 工具分级:将只读查询、数据修改、外部网络访问和系统命令拆成不同工具,不能把所有能力塞进一个万能工具。
3. 参数白名单:对枚举值、文件名、分页大小、URL 域名、SQL 查询类型进行白名单校验。不要仅依赖模型生成的参数。
4. 防止间接提示注入:文件、网页、Issue 和数据库记录都可能包含“忽略之前指令”之类的文本。工具应把外部内容标识为不可信数据,Client 不应因为数据中的指令自动执行其他高风险工具。
5. 防止 SSRF:如果工具需要请求 URL,只允许固定域名和 HTTPS,解析 DNS 后还要阻断回环地址、链路本地地址、私有地址和云元数据地址。生产环境最好通过出口代理统一控制。
6. 敏感信息脱敏:日志中不要记录 Authorization、Cookie、Refresh Token、完整手机号、身份证号和数据库连接串。返回模型的内容也应进行字段级脱敏。
7. 认证与授权:远程 MCP Server 不能只靠一个静态共享密钥。应使用 TLS、短期凭证、服务端身份校验、作用域和过期时间,并在 Server 侧再次检查用户权限。
8. 审计:记录调用者、工具名、参数摘要、结果状态、耗时和拒绝原因。参数摘要应脱敏,日志目录本身也需要访问控制。
9. 资源限制:限制请求体、返回体、并发数、单次执行时间和重试次数,防止模型循环调用造成资源耗尽。
10. 版本管理:固定 SDK 和依赖版本,启用依赖漏洞扫描,并在升级前重新执行工具权限和异常处理测试。
## 七、本地安全验证方法
所有验证应在本地实验目录或明确授权的靶场进行。可以准备以下测试用例:
- 传入 `../readme.txt`,应被拒绝;
- 传入绝对路径,应被拒绝;
- 在允许目录中创建指向外部文件的符号链接,应被拒绝;
- 读取超过 4096 字节的文件,应返回大小限制错误;
- 传入空字符串、超长字符串和非字符串 JSON 类型,应被稳定拒绝;
- 在文本中写入伪造的模型指令,确认 Server 只返回内容,不会因此自动调用其他工具;
- 并发发起大量只读请求,确认有超时、并发限制或队列保护;
- 终止 Server 进程,确认 Client 能识别连接断开,不会无限重试。
可以用单元测试验证路径边界:
```bash
python -m pytest -q
python -m compileall server.py
```
若 Server 需要访问数据库,建议使用本地容器和测试数据,并为只读账号设置数据库层面的 `SELECT` 权限,而不是只在 Python 代码中约束 SQL。对查询工具应优先采用参数化查询,禁止把模型生成的字符串直接拼接进 SQL、Shell 或模板表达式。
## 八、常见问题
问:为什么工具已经是只读的,还需要审批?
答:只读不等于无风险。文件内容、客户数据和内部配置仍可能泄露,且读出的内容可能诱导模型继续调用其他工具。审批可以帮助确认调用范围和数据敏感性。
问:stdio 模式是否天然安全?
答:不是。stdio 通常减少了网络暴露面,但 Server 仍可能拥有过高的本地操作系统权限,且 Client 配置、日志和环境变量仍可能泄露秘密。它只是传输方式,不是完整的安全边界。
问:为什么不能只在工具描述中写“禁止访问敏感文件”?
答:工具描述属于提示层约束,不能代替程序化授权。攻击者或异常模型输出仍可能构造路径穿越、符号链接或边界类型,真正的限制必须在 Server 代码、操作系统权限和数据源权限中执行。
问:远程 MCP Server 最需要关注什么?
答:重点是传输加密、Server 身份认证、调用者授权、重放防护、速率限制、审计和网络出口控制。尤其要避免把“能访问内网”的 Server 直接暴露给公网。
## 九、总结
MCP 的价值在于以统一协议连接模型和外部能力,但每一个 Tool 都应被当作一个新的受控 API 端点,而不是普通提示词。实践中应先从只读、低权限、固定目录或测试数据库开始,明确输入 Schema 和返回边界,再逐步增加能力。安全设计的核心不是相信模型会正确选择工具,而是让 Server 在模型选择错误、输入恶意、数据包含提示注入、请求异常或 Client 被滥用时,仍能通过程序化校验、操作系统权限、网络隔离和审计机制把影响限制在可接受范围内。
MCP(Model Context Protocol)是一套让大模型客户端以统一方式发现并调用外部工具、读取资源、使用提示模板的协议。它解决的不是“如何让模型变聪明”,而是“如何把模型与本地文件、数据库、内部 API、代码仓库或业务系统连接起来”。在实际接入中,MCP Server 往往拥有比普通聊天插件更高的权限,例如读取文件、执行查询、访问网络或调用企业内部接口,因此它同时扩大了能力边界和攻击面。
从移动应用安全工程的视角看,MCP Server 可以理解为一个新的本地或远程服务入口:Client 负责发起初始化、能力协商和工具调用,Server 负责暴露工具、资源和提示模板。真正需要重点关注的是工具参数校验、身份认证、最小权限、敏感数据处理、日志审计,以及模型生成内容不可信这一基本前提。本文以本地授权实验为例,演示如何实现一个只读的 MCP Server,并给出客户端接入、权限控制和安全验证方法。
## 二、协议原理与核心对象
MCP 通常基于 JSON-RPC 2.0 进行消息交互。通信双方首先通过初始化流程协商协议版本和能力,随后 Client 可以请求 Server 列出可用工具,Server 返回工具名称、说明和输入参数 Schema。Client 再根据用户意图选择工具并发送调用请求,Server 完成参数验证和业务处理后返回结果。
常见概念包括:
1. Server:提供工具、资源或提示模板的一方。它可以是本地进程,也可以通过 HTTP 等方式部署在远程环境。
2. Client:连接 MCP Server 的宿主程序,例如支持 MCP 的桌面应用、开发工具或自研 Agent。
3. Tool:可被调用的动作,例如查询数据库、读取固定目录文件、检查 APK 元数据。Tool 可能产生副作用,因此必须明确权限和确认机制。
4. Resource:供 Client 读取的上下文资源,例如文档、配置摘要或只读数据。
5. Prompt:可复用的提示模板,用于规范某类任务的输入结构。
6. Transport:消息传输方式。开发阶段常见的是 stdio,远程部署则可能使用基于 HTTP 的传输方式。不同 SDK 的具体接口名称和版本可能存在差异,应以所使用 SDK 的官方文档为准。
## 三、实验目标与前置条件
本文实现一个本地只读 MCP Server,提供两个工具:列出允许目录下的文件,以及读取指定文本文件的前若干字符。它不会执行 Shell 命令,不会访问任意路径,也不会把环境变量、令牌或完整系统信息暴露给模型。
前置条件如下:
- Python 3.10 或更高版本;
- 一个支持 MCP 的本地 Client,或者能够执行 MCP stdio Server 的测试客户端;
- 独立的实验目录,例如 ~/mcp-lab;
- 一个非生产测试账号或本地虚拟环境;
- 不要把真实生产密钥、用户隐私数据和企业源代码放入实验目录。
创建项目目录并初始化虚拟环境:
```bash
mkdir -p ~/mcp-lab/safe-data
cd ~/mcp-lab
python3 -m venv .venv
. .venv/bin/activate
python -m pip install --upgrade pip
```
如果使用的 MCP Python SDK 包名或导入路径与示例不同,应按照当前 SDK 文档调整。依赖安装后,建议执行 `pip freeze` 保存版本,避免因 SDK 升级导致接口行为变化。
## 四、分步骤实现一个最小只读 Server
### 第一步:准备受控数据目录
```bash
printf '这是用于本地测试的公开样例文件。\n' > ~/mcp-lab/safe-data/readme.txt
printf '仅用于验证路径过滤和编码处理。\n' > ~/mcp-lab/safe-data/example.txt
```
不要直接把家目录、项目根目录或系统临时目录作为可读目录。目录边界是安全设计的第一道防线。
### 第二步:编写工具逻辑
下面给出接近常见 Python MCP SDK 风格的示例。不同 SDK 的装饰器、启动方法和返回类型可能不同,重点是数据流和安全校验逻辑,而不是机械复制导入语句:
```python
from pathlib import Path
BASE_DIR = Path.home() / "mcp-lab" / "safe-data"
MAX_BYTES = 4096
def safe_path(name: str) -> Path:
if not isinstance(name, str) or not name.strip():
raise ValueError("name must be a non-empty string")
candidate = (BASE_DIR / name).resolve()
base = BASE_DIR.resolve()
if candidate != base and base not in candidate.parents:
raise ValueError("path is outside the allowed directory")
if candidate.is_symlink():
raise ValueError("symbolic links are not allowed")
return candidate
def list_files() -> list[str]:
result = []
for item in BASE_DIR.iterdir():
if item.is_file() and not item.is_symlink():
result.append(item.name)
return sorted(result)
def read_text(name: str) -> str:
path = safe_path(name)
if not path.is_file():
raise FileNotFoundError("file does not exist")
data = path.read_bytes()
if len(data) > MAX_BYTES:
raise ValueError("file is too large")
return data.decode("utf-8", errors="strict")
```
这里有几个关键点:第一,使用 `resolve()` 处理 `../` 等路径穿越;第二,禁止符号链接,避免链接把访问范围绕到允许目录之外;第三,限制单文件大小,防止大文件拖垮 Client 或上下文窗口;第四,指定 UTF-8 严格解码,避免把不可控二进制内容当成普通文本处理。
### 第三步:注册 MCP 工具
在实际 SDK 中,通常需要创建 Server 实例,并使用工具注册装饰器或显式注册函数:
```python
# 以下为结构示例,具体 import 和启动函数以所用 SDK 版本为准
server = Server("safe-files")
@server.tool()
def list_allowed_files() -> list[str]:
"""列出实验目录中的普通文件,不读取文件内容。"""
return list_files()
@server.tool()
def read_allowed_text(name: str) -> str:
"""读取实验目录内指定 UTF-8 文本文件,最多返回 4096 字节。"""
return read_text(name)
if __name__ == "__main__":
server.run_stdio()
```
工具描述应准确、简短且不能夸大权限。不要写成“读取任意文件”或“执行系统操作”,否则 Client 可能在错误场景下选择该工具。工具返回值也应尽量结构化,例如使用 `{"ok": true, "content": "..."}`,并对错误进行统一处理,避免把堆栈、环境变量或本地路径泄露给调用方。
## 五、在 Client 中接入
以本地 stdio Client 为例,配置中通常需要指定启动命令、工作目录和环境变量。示意配置如下:
```json
{
"mcpServers": {
"safe-files": {
"command": "/home/test/mcp-lab/.venv/bin/python",
"args": ["/home/test/mcp-lab/server.py"],
"env": {
"PYTHONUNBUFFERED": "1"
}
}
}
}
```
路径必须使用实际绝对路径,并尽量使用专门的低权限操作系统账号运行。不要在配置文件中直接写入 API Key、数据库密码或长期 Token。若 Client 支持工具审批,应开启“每次调用前确认”,特别是存在写入、网络请求、代码执行或支付操作的工具。对于只读工具,也建议在初期保留调用确认和详细日志,确认行为稳定后再按风险降低交互频率。
## 六、权限与安全设计清单
1. 最小权限:Server 进程只授予完成任务所需的目录、网络和数据库权限。读取文件的 Server 不应拥有写文件和执行命令权限。
2. 工具分级:将只读查询、数据修改、外部网络访问和系统命令拆成不同工具,不能把所有能力塞进一个万能工具。
3. 参数白名单:对枚举值、文件名、分页大小、URL 域名、SQL 查询类型进行白名单校验。不要仅依赖模型生成的参数。
4. 防止间接提示注入:文件、网页、Issue 和数据库记录都可能包含“忽略之前指令”之类的文本。工具应把外部内容标识为不可信数据,Client 不应因为数据中的指令自动执行其他高风险工具。
5. 防止 SSRF:如果工具需要请求 URL,只允许固定域名和 HTTPS,解析 DNS 后还要阻断回环地址、链路本地地址、私有地址和云元数据地址。生产环境最好通过出口代理统一控制。
6. 敏感信息脱敏:日志中不要记录 Authorization、Cookie、Refresh Token、完整手机号、身份证号和数据库连接串。返回模型的内容也应进行字段级脱敏。
7. 认证与授权:远程 MCP Server 不能只靠一个静态共享密钥。应使用 TLS、短期凭证、服务端身份校验、作用域和过期时间,并在 Server 侧再次检查用户权限。
8. 审计:记录调用者、工具名、参数摘要、结果状态、耗时和拒绝原因。参数摘要应脱敏,日志目录本身也需要访问控制。
9. 资源限制:限制请求体、返回体、并发数、单次执行时间和重试次数,防止模型循环调用造成资源耗尽。
10. 版本管理:固定 SDK 和依赖版本,启用依赖漏洞扫描,并在升级前重新执行工具权限和异常处理测试。
## 七、本地安全验证方法
所有验证应在本地实验目录或明确授权的靶场进行。可以准备以下测试用例:
- 传入 `../readme.txt`,应被拒绝;
- 传入绝对路径,应被拒绝;
- 在允许目录中创建指向外部文件的符号链接,应被拒绝;
- 读取超过 4096 字节的文件,应返回大小限制错误;
- 传入空字符串、超长字符串和非字符串 JSON 类型,应被稳定拒绝;
- 在文本中写入伪造的模型指令,确认 Server 只返回内容,不会因此自动调用其他工具;
- 并发发起大量只读请求,确认有超时、并发限制或队列保护;
- 终止 Server 进程,确认 Client 能识别连接断开,不会无限重试。
可以用单元测试验证路径边界:
```bash
python -m pytest -q
python -m compileall server.py
```
若 Server 需要访问数据库,建议使用本地容器和测试数据,并为只读账号设置数据库层面的 `SELECT` 权限,而不是只在 Python 代码中约束 SQL。对查询工具应优先采用参数化查询,禁止把模型生成的字符串直接拼接进 SQL、Shell 或模板表达式。
## 八、常见问题
问:为什么工具已经是只读的,还需要审批?
答:只读不等于无风险。文件内容、客户数据和内部配置仍可能泄露,且读出的内容可能诱导模型继续调用其他工具。审批可以帮助确认调用范围和数据敏感性。
问:stdio 模式是否天然安全?
答:不是。stdio 通常减少了网络暴露面,但 Server 仍可能拥有过高的本地操作系统权限,且 Client 配置、日志和环境变量仍可能泄露秘密。它只是传输方式,不是完整的安全边界。
问:为什么不能只在工具描述中写“禁止访问敏感文件”?
答:工具描述属于提示层约束,不能代替程序化授权。攻击者或异常模型输出仍可能构造路径穿越、符号链接或边界类型,真正的限制必须在 Server 代码、操作系统权限和数据源权限中执行。
问:远程 MCP Server 最需要关注什么?
答:重点是传输加密、Server 身份认证、调用者授权、重放防护、速率限制、审计和网络出口控制。尤其要避免把“能访问内网”的 Server 直接暴露给公网。
## 九、总结
MCP 的价值在于以统一协议连接模型和外部能力,但每一个 Tool 都应被当作一个新的受控 API 端点,而不是普通提示词。实践中应先从只读、低权限、固定目录或测试数据库开始,明确输入 Schema 和返回边界,再逐步增加能力。安全设计的核心不是相信模型会正确选择工具,而是让 Server 在模型选择错误、输入恶意、数据包含提示注入、请求异常或 Client 被滥用时,仍能通过程序化校验、操作系统权限、网络隔离和审计机制把影响限制在可接受范围内。