15,465 个 MCP 服务器,0 治理:OX Security 的审计对你的 Agent 栈意味着什么

·阅读约11分钟·Evergreen Tools Team

企业安全团队花了十年时间,围绕云环境建立治理:数据驻留要求、零信任边界、精细的 IAM 策略、供应链审计。这些问题的答案都有一个前提——你大概知道数据落在哪里、谁在运营那台基础设施。2026 年 9 月 24 日,OX Security 发布了一份名为《15,465 MCP Servers, 0 Governance》的研究,结论是:Model Context Protocol 正在让大部分这类治理失效。MCP 让 AI Agent、开发者工作站和自动化流水线按需连上第三方服务器,这很有用,但很多时候它就是一套影子 AI 基础设施。

一、这项研究到底测了什么

研究的方法很直接:从三个公开注册表——mcp-official-registry、cline-marketplace 和 github-mcp-registry——收集了 15,465 个已发布的 MCP 服务器,然后收窄到 5,095 个唯一主机名做基础设施分析。MCP 是 Anthropic 在 2024 年 11 月引入的开放标准,让 AI 应用连接外部工具与数据源。但正如 OX 所指出的,MCP 本身并不规定服务器应该跑在哪里、由谁运营、可以接收什么数据,也不保证一个在线服务器背后部署的代码和它公开的源码一致。这些决定基本都甩给了接入方。研究负责人 Moshe Siman Tov Bustan 的总结很到位:你能审查代码,但服务器可能是另一个版本;你也不知道谁控制着它、它收集什么数据、未来的更新会改什么。

# Finding the MCP servers you are actually running is step one.
# Every config file, every IDE, every pipeline is a candidate.

import json, pathlib

CONFIG_HINTS = [".mcp.json", "mcp.json", ".cursor/mcp.json",
                ".vscode/mcp.json", "claude_desktop_config.json"]

def inventory(root: pathlib.Path):
    found = []
    for path in root.rglob("*"):
        if path.name in [p.split("/")[-1] for p in CONFIG_HINTS] and path.is_file():
            try:
                cfg = json.loads(path.read_text())
            except Exception:
                continue
            for name, spec in (cfg.get("mcpServers") or {}).items():
                found.append({
                    "server": name,
                    "command": spec.get("command"),
                    "url": spec.get("url"),
                    "declared_in": str(path.relative_to(root)),
                })
    return found

for row in inventory(pathlib.Path.home()):
    print(row["server"], row["url"] or row["command"])
数据中心里的服务器机架

MCP 让 Agent 按需连上第三方服务器

二、发现之一:数据驻留的盲区

第一个发现是数据驻留盲区。在 5,095 个主机名中,15.6%(也就是 796 个)解析到美国境外的基础设施,其中 19 个在中国、18 个在俄罗斯。关键在于,MCP 在协议层面根本没有地理区域的概念。一家企业可以在自己的云工作负载上执行严格的数据驻留控制,同时它的 AI Agent 却自由地连向这些控制之外的服务器。示例 3 给出了把这件看不见的事变成可查询事实的做法:把每个主机名做解析与注册信息核查,判断它是否仍然存在、注册时间与到期时间,并把「最近注册的便宜域名」标出来复核。

// Governance gate 1: egress is deny-by-default. MCP has no protocol-level
// concept of geography, so the boundary has to live in your network.

const mcpEgressPolicy = {
  default: "deny",
  rules: [
    { server: "github-mcp", allow: ["api.github.com"], ports: [443] },
    { server: "postgres-mcp", allow: ["db.internal.corp"], ports: [5432] },
  ],
  // Anything that does not match is blocked AND recorded. A blocked
  // request you can see beats an allowed request you never noticed.
  onBlock: (req) => audit.record({ kind: "mcp_egress_blocked", ...req }),
};

export function checkEgress(server, host, port) {
  const rule = mcpEgressPolicy.rules.find((r) => r.server === server);
  if (!rule || !rule.allow.includes(host) || !rule.ports.includes(port)) {
    mcpEgressPolicy.onBlock({ server, host, port });
    return false;
  }
  return true;
}

三、发现之二与之三:家庭网络与废弃域名

第二个和第三个发现更贴近运维现实。0.45% 的主机名经由消费级 ISP 网络或个人内网穿透工具路由,也就是说,生产级 AI 工作流可能依赖着没有可用性保障、没有企业访问控制、也没有真正可审计性的基础设施——因为它本来就不是按企业基建造的。另一头,2.3% 的主机名已经无法解析,其中六个域名处于未注册状态,年费只要 4 到 12 美元。这些域名很可能仍被写在某些人的配置或流水线里。任何人都可以买下它们,然后开始冒充它原本指向的服务器。这是最经典的一类接管路径:信任关系还在,被信任的对象已经易主。

#!/usr/bin/env python3
# Governance gate 2: resolve every hostname. Where does it live, does it
# still exist, and could someone else point it somewhere new tomorrow?

import socket, whois  # whois via python-whois

def classify(hostname):
    try:
        ip = socket.gethostbyname(hostname)
    except socket.gaierror:
        return {"hostname": hostname, "status": "does_not_resolve",
                "risk": "abandoned domain may be re-registered"}

    record = whois.whois(hostname)
    created = record.creation_date
    expires = record.expiration_date
    return {
        "hostname": hostname,
        "ip": ip,
        "status": "resolves",
        "created": str(created),
        "expires": str(expires),
        # Cheap, recently created domains deserve a second look before
        # you let an agent send them your code and credentials.
        "risk": "review" if is_recent(created) else "ok",
    }

for host in reported_hostnames:
    print(classify(host))
安全数据分析

5,095 个主机名里有 796 个解析到美国境外

四、发现之四:比一次授权活得更久的信任

第四个发现最值得安全团队警惕。OX 做了一个基于「信任」的提示注入测试:他们拿 Claude Code 搭配 Haiku 3.5,让一个恶意 MCP 服务器先请求访问一个无害文件;用户以 always-allow 批准。随后服务器请求一个敏感文件,其中包括 .env,并没有再弹窗就直接拿到了。同一个攻击在 Opus 4.6 和 4.7 上被检测并拦截。Anthropic 的回应是:一旦授予 always-allow,这就是有文档记录的行为;而模型层面对恶意内容的检测是尽力而为的启发式,不是安全边界。这句话值得反复读:你用一次点击换来的便利,会变成一次永久的授权。

// Governance gate 3: never grant always-allow. One benign approval should
// not become permanent access to every file the server asks for later.
// The OX Security test showed a single always-allow on a harmless file
// unlocked a sensitive .env read with no further prompt.

const permissionModel = {
  // Approvals are per (server, resource), never per server.
  grant: (server, resource) => ({
    server,
    resource,                 // exact path/pattern, not "*"
    expiresInMinutes: 30,     // time-boxed, not permanent
    maxUses: 1,               // single use unless reviewed
  }),
  deny: ["**/.env*", "**/.ssh/**", "**/id_rsa*", "**/.aws/**", "**/secrets/**"],
  alwaysAllow: false,         // this whole option is off
  onRequest: async (req) => {
    if (permissionModel.deny.some((p) => minimatch(req.resource, p))) {
      return deny(req);
    }
    return promptHuman(req);  // fresh consent, every time
  },
};

五、从部署前评审转向运行时控制

把这些发现放到一起,结论并不是「不要用 MCP」——MCP 的价值是真实的,规模也在飞速增长。结论是:治理必须从「部署前评审」变成「运行时控制」。示例 2 把出口做成默认拒绝的白名单:MCP 没有协议级的地理概念,边界就必须落在你的网络里;每条规则按服务器和端口细化,任何不匹配的请求不仅被拦截,还要写进审计。示例 4 则把工具调用和目的地绑定记录在一起,这样「哪个 Agent 把数据发到了我们批准区域之外」就变成一句可执行的查询,而不是一次事故调查。

# Governance gate 4: log the tool call and the destination together, so a
# question like "which agent talked to a host outside our region?" is a query
# rather than an incident investigation.

def log_tool_call(session, call, destination, decision):
    ledger.append({
        "ts": time.time(),
        "agent": session.agent,
        "server": call.server,
        "tool": call.tool,                 # e.g. read_file, run_query
        "destination": destination.host,   # where the bytes went
        "residency": destination.region,   # us | eu | other | unknown
        "decision": decision,              # allow | block | ask
        "resourceHash": call.resource_hash,
        "approvedBy": decision.approver,
    })

# The monthly question: how much traffic left our approved regions?
outside = [r for r in ledger.tail(10000) if r["residency"] not in ("us", "eu")]
print(len(outside), "tool calls reached unapproved regions")
审计与日志

未声明的出口应当产生审计记录,而不是静默通过

六、一份七步落地清单

一份可落地的清单:第一,先清点——你有多少 MCP 服务器、分别写在哪些配置里、连向哪些主机,示例 1 给出了从配置中发现服务器的做法。第二,出口默认拒绝,任何未声明的目的地都产生审计记录。第三,对每个主机名做解析与注册核查,把不再解析的域名挑出来并阻断,把最近注册的低价域名列入复核。第四,禁止 always-allow,把授权改成「按服务器 + 按资源」、带时限、单次使用。第五,为每个 MCP 服务器使用独立凭据并收窄作用域,别让一个服务器拿着你全量的令牌。第六,把工具调用与出口目的地一起留痕,并定期出「离开批准区域的调用数」这类报表。第七,把 MCP 加进采购与供应商评审,问清楚谁运营服务器、数据去哪里、更新由谁控制。

📌 常见问题 FAQ

OX Security 这项研究分析了多少 MCP 服务器?

研究从三个公开注册表收集了 15,465 个已发布的 MCP 服务器,并收窄到 5,095 个唯一主机名进行基础设施分析。

有多少 MCP 主机名解析到美国境外?

15.6%(5,095 个中的 796 个)解析到美国境外的基础设施,其中包括 19 个在中国、18 个在俄罗斯。MCP 在协议层面没有地理区域的概念。

什么是「废弃域名」风险?

2.3% 的主机名已无法解析,其中六个域名未注册,年费仅 4 到 12 美元。它们可能仍写在配置或流水线里,任何人都能买下并冒充原端点。

always-allow 权限有什么问题?

OX 的测试显示,用 Claude Code 搭配 Haiku 3.5 时,一次针对无害文件的 always-allow 批准会让恶意 MCP 服务器在不再弹窗的情况下读取 .env 等敏感文件。该攻击在 Opus 4.6 和 4.7 上被拦截。

组织应该怎样降低 MCP 风险?

把治理从部署前评审转向运行时控制:出口默认拒绝、按主机名核查解析与注册、禁止 always-allow、为每个服务器使用独立凭据,并把工具调用与出口目的地一起留痕。