查询 45 次,耗时 0.2849 秒

从零实现一个安全的 MCP Server:本地 APK 元数据工具接入、权限边界与 Client 调试

BOSS

首领

BOSS Rep
0
0
0
Rep
0
BOSS Vouches
Vouches
0
3 YEARS
3 YEARS OF SERVICE
创始人
BOSS 获得
0
0
0
获得
0
BOSS 凭证
0
0
0
凭证
0
主题
19
点赞
16
3 年
来到社区 3 年
LEVEL 100 XP
 
一、背景:为什么 MCP 工具不能只关注“能不能调用”

MCP(Model Context Protocol)用于在模型应用与外部数据源、工具和提示模板之间建立标准化连接。一个典型架构由 MCP Host、MCP Client 和 MCP Server 组成:Host 是承载模型与交互界面的应用;Client 负责与某个 Server 建立会话;Server 则通过协议暴露 Tools、Resources 或 Prompts。

以移动应用安全分析为例,我们可能希望让模型读取本地授权样本的 APK 基本信息、计算文件哈希、列出压缩包内的文件,但又不希望它读取整个硬盘、执行任意 shell 命令或把样本上传到第三方服务。MCP 解决了“如何接入工具”的问题,却不会自动替开发者完成权限隔离。Server 暴露的每一个 Tool,本质上都是新增攻击面。

本文将在纯本地实验环境中实现一个只读 APK 元数据 MCP Server,并通过 MCP Inspector 进行调试。示例不连接真实业务系统,不包含绕过认证或攻击真实目标的操作。

二、MCP 的基本工作原理

MCP 消息采用 JSON-RPC 2.0 风格。建立连接后,Client 与 Server 会先进行初始化和能力协商,再按 Server 声明的能力执行操作。常见交互包括:

1. initialize:协商协议版本、客户端信息和服务端能力。
2. tools/list:获取 Server 提供的工具名称、描述和输入结构。
3. tools/call:按工具名称和参数发起调用。
4. resources/list、resources/read:枚举或读取资源。
5. prompts/list、prompts/get:获取服务端提供的提示模板。

本地集成常使用 stdio 传输:Host 启动 Server 子进程,并通过标准输入、标准输出交换协议消息。网络部署可使用当前 SDK 支持的 HTTP 类传输;不同 SDK 版本可能仍出现旧式 SSE 配置,实施时应以所用 SDK 的官方文档和版本说明为准。

一个非常重要的边界是:模型提出调用建议,不等于系统必须执行。Client 或 Host 应在高风险调用前进行确认;Server 也必须独立验证参数,不能把安全责任完全交给提示词。

三、实验目标与安全模型

本次 Server 提供两个工具:

- apk_sha256:计算授权 APK 样本的 SHA-256。
- apk_list_entries:读取 APK 这个 ZIP 容器中的文件名,结果限制条数。

安全约束如下:

- 只能访问环境变量 MCP_ARTIFACT_DIR 指定的目录。
- 只接受 .apk 文件。
- 拒绝 ../ 等路径穿越。
- 拒绝符号链接,避免链接跳出沙箱目录。
- 限制文件大小、ZIP 条目总量及单次返回数量。
- 不执行 aapt、jadx、apktool 或任意 shell 命令。
- 不返回 APK 文件内容和签名私钥等敏感材料。

四、前置条件

建议使用独立测试目录,Python 3.10 或更高版本,并确保本机已安装 Node.js,以便运行 MCP Inspector。

创建环境:

mkdir -p ~/mcp-apk-lab/samples
cd ~/mcp-apk-lab
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
pip install "mcp[cli]"

请只把自己拥有或明确获准分析的 APK 放入 samples。不要把生产签名文件、用户数据备份或来源不明的敏感样本混入目录。

五、实现安全的 MCP Server

在 ~/mcp-apk-lab 下创建 server.py:

from pathlib import Path
import hashlib
import os
import zipfile

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("Local APK Metadata Lab")

ROOT = Path(os.environ.get("MCP_ARTIFACT_DIR", "./samples")).expanduser().resolve()
MAX_APK_SIZE = 500 * 1024 * 1024
MAX_ZIP_ENTRIES = 20000


def resolve_apk(name: str) -> Path:
if not name or "\x00" in name:
raise ValueError("文件名无效")

candidate = (ROOT / name).resolve()

try:
candidate.relative_to(ROOT)
except ValueError:
raise ValueError("禁止访问样本目录之外的路径")

if candidate.suffix.lower() != ".apk":
raise ValueError("只允许处理 .apk 文件")
if not candidate.exists() or not candidate.is_file():
raise FileNotFoundError("APK 不存在")
if candidate.is_symlink():
raise ValueError("不允许处理符号链接")
if candidate.stat().st_size > MAX_APK_SIZE:
raise ValueError("APK 超过实验环境的大小限制")

return candidate


@mcp.tool()
def apk_sha256(name: str) -> dict:
"""计算本地授权 APK 样本的 SHA-256,不读取样本目录外的文件。"""
path = resolve_apk(name)
digest = hashlib.sha256()

with path.open("rb") as fp:
for block in iter(lambda: fp.read(1024 * 1024), b""):
digest.update(block)

return {
"file": path.name,
"size": path.stat().st_size,
"sha256": digest.hexdigest(),
}


@mcp.tool()
def apk_list_entries(name: str, limit: int = 100) -> dict:
"""列出 APK ZIP 容器中的部分条目,不解压文件。"""
path = resolve_apk(name)
limit = max(1, min(limit, 500))

try:
with zipfile.ZipFile(path, "r") as archive:
infos = archive.infolist()
if len(infos) > MAX_ZIP_ENTRIES:
raise ValueError("ZIP 条目数量超过安全限制")

entries = [
{
"name": item.filename,
"compressed_size": item.compress_size,
"size": item.file_size,
}
for item in infos[:limit]
]
except zipfile.BadZipFile:
raise ValueError("文件不是有效的 APK/ZIP 容器")

return {
"file": path.name,
"total_entries": len(infos),
"returned_entries": len(entries),
"entries": entries,
}


if __name__ == "__main__":
ROOT.mkdir(parents=True, exist_ok=True)
mcp.run(transport="stdio")

这里没有直接接受绝对路径,也没有把参数拼接进命令行。即使模型传入 ../../etc/passwd,resolve 后的路径也无法通过 relative_to(ROOT) 检查。后缀检查只是业务约束,真正的目录边界仍由规范化路径检查负责。

六、启动与调试

先进行基础语法检查:

source ~/mcp-apk-lab/.venv/bin/activate
python -m py_compile ~/mcp-apk-lab/server.py

随后用 MCP Inspector 启动:

MCP_ARTIFACT_DIR="$HOME/mcp-apk-lab/samples" npx -y @modelcontextprotocol/inspector "$HOME/mcp-apk-lab/.venv/bin/python" "$HOME/mcp-apk-lab/server.py"

Inspector 启动后,连接本地 Server,查看 Tools 列表。可以使用以下参数测试:

工具:apk_sha256
参数:{"name":"demo.apk"}

工具:apk_list_entries
参数:{"name":"demo.apk","limit":20}

还应主动执行拒绝性测试:

{"name":"../outside.apk"}
{"name":"/etc/passwd"}
{"name":"demo.txt"}
{"name":"missing.apk"}
{"name":"demo.apk","limit":999999}

预期行为是越界路径和非 APK 被拒绝,过大的 limit 被收敛到 500,不存在的文件返回明确错误。安全测试只应针对本地实验目录。

注意:stdio 模式下,stdout 通常用于协议通信。不要随意用 print 向 stdout 输出调试日志,否则可能干扰协议消息。日志应写到 stderr,或使用正确配置的日志框架。

七、接入 MCP Client

支持 MCP 的桌面 Host 通常允许配置本地 stdio Server。不同产品的配置文件位置有所不同,但核心配置类似:

{
"mcpServers": {
"apk-lab": {
"command": "/home/user/mcp-apk-lab/.venv/bin/python",
"args": ["/home/user/mcp-apk-lab/server.py"],
"env": {
"MCP_ARTIFACT_DIR": "/home/user/mcp-apk-lab/samples"
}
}
}
}

应使用绝对路径,避免 Host 的工作目录不同导致 Server 找不到虚拟环境或样本目录。修改配置后一般需要重启 Host。不要把令牌、云密钥或生产数据库密码直接写入可同步的配置文件。

八、权限与安全设计要点

1. 防止“混淆代理”问题

模型可能受到不可信文档、网页或 APK 内文本的提示注入影响。例如样本中的文本要求模型调用其他高权限工具。Server 不应因为请求来自模型就默认可信。读取工具与写入工具应分开部署,高风险工具需要人工确认。

2. 最小权限

本例只读取专用 samples 目录。生产环境还可使用独立低权限系统账号、只读挂载、容器文件系统限制和资源配额。不要以 root 身份运行普通 MCP Server。

3. 参数校验不能只依赖 Schema

类型结构可以帮助 Client 生成正确参数,但无法替代服务端校验。目录范围、文件大小、条目数量、超时、并发量和返回体大小都要在 Server 内强制限制。

4. 避免通用命令执行工具

诸如 run_shell(command) 看似灵活,实际上把整个系统权限暴露给模型。更合理的做法是定义窄工具,例如 get_apk_hash、read_manifest_summary,并使用固定参数调用底层程序。若必须启动子进程,应使用参数数组、固定可执行文件、超时和隔离环境,禁止 shell=True。

5. 网络传输的额外要求

如果把 Server 从 stdio 改为网络服务,应增加 TLS、可靠的身份认证、细粒度授权、请求大小限制、速率限制和审计。认证回答“调用者是谁”,授权回答“调用者能调用哪个工具、访问哪个资源”。两者不能混为一谈。

对本机 HTTP 服务还应校验 Origin、谨慎绑定监听地址,并防范 DNS rebinding。没有远程访问需求时优先绑定回环地址,而不是 0.0.0.0。令牌不应出现在 URL 查询参数或普通日志中。

6. 审计与隐私

建议记录时间、调用主体、工具名、结果状态、耗时和经过脱敏的参数摘要。不要默认记录 APK 内容、访问令牌或完整用户输入。文件哈希可用于关联分析记录,但它也可能成为敏感样本标识,应按组织策略保存。

九、常见问题

问题一:Client 能发现 Server,但工具列表为空。

先检查 SDK 版本、Server 是否在初始化前完成工具注册,以及 Host 是否缓存旧的能力列表。可先用 Inspector 排除 Host 配置问题。

问题二:Server 一启动就断开。

常见原因包括 Python 路径错误、依赖安装在另一个虚拟环境、stdout 出现非协议输出、环境变量缺失,或者 Host 无权访问脚本和样本目录。应从 stderr 日志定位。

问题三:为什么不直接提供“分析任意文件”工具?

因为参数越通用,权限越难约束。MCP 工具应围绕明确任务设计,而不是成为文件系统或操作系统的万能入口。

问题四:限制 ZIP 条目数量是否足够防止压缩炸弹?

不够。本文仅列元数据且不解压,风险较低;如果后续增加解压或内容读取,还必须限制累计解压大小、单条目大小、压缩比、嵌套层数、处理时间和输出目录,并再次检查 ZIP Slip 路径穿越。

问题五:工具描述真的属于安全控制吗?

描述可以帮助模型正确选择工具,但不是强制安全边界。真正的边界必须由代码校验、操作系统权限、沙箱和授权策略实现。

十、总结

一个可用的 MCP Server 不只是把 Python 函数加上装饰器。完整工程至少应包含清晰的工具语义、严格的输入校验、最小文件与网络权限、资源限制、错误处理、审计以及 Client 侧确认机制。

本文的 APK 元数据 Server 展示了一个推荐模式:让工具保持单一用途,只访问专用授权目录,不执行通用命令,并同时测试正常路径与拒绝路径。后续如需增加 AndroidManifest.xml 摘要、证书指纹或 iOS IPA 元数据读取,也应继续采用窄接口、只读处理和沙箱隔离,而不是逐步扩张为不受控的远程执行入口。
 

黄先生嗯

New member

黄先生嗯 Rep
0
0
0
Rep
0
黄先生嗯 Vouches
Vouches
0
黄先生嗯 获得
0
0
0
获得
0
黄先生嗯 凭证
0
0
0
凭证
0
主题
2
点赞
0
HakB
0 月
在社区成长 0 月
LEVEL 1 1 XP
 
有一个容易被忽略的实现细节:candidate 经过 resolve() 后,若原路径是符号链接,candidate.is_symlink() 通常已返回 False;虽然链接到目录外会被 relative_to() 拦截,但“拒绝所有符号链接”的语义并未真正实现。建议先对 (ROOT/name) 使用 lstat() 检查各级路径,再做 resolve 和边界校验。测试时可创建 samples/link.apk 指向样本文件,确认调用明确拒绝;同时建议累计校验 ZIP 条目的 file_size,避免大量小条目或高压缩比内容造成资源消耗。
 
顶部