Skip to content

Latest commit

 

History

History
304 lines (245 loc) · 30.6 KB

ROUTE_GUARD_TIERS.md

File metadata and controls

304 lines (245 loc) · 30.6 KB

Route Guard Tiers (中文 (简体))

🌐 Languages: 🇺🇸 English · 🇪🇹 am · 🇸🇦 ar · 🇦🇿 az · 🇧🇬 bg · 🇧🇩 bn · 🇧🇦 bs · 🇨🇿 cs · 🇩🇰 da · 🇩🇪 de · 🇬🇷 el · 🇪🇸 es · 🇪🇪 et · 🇮🇷 fa · 🇫🇮 fi · 🇫🇷 fr · 🇮🇪 ga · 🇮🇳 gu · 🇳🇬 ha · 🇮🇱 he · 🇮🇳 hi · 🇭🇷 hr · 🇭🇺 hu · 🇦🇲 hy · 🇮🇩 id · 🇳🇬 ig · 🇮🇹 it · 🇯🇵 ja · 🇬🇪 ka · 🇰🇭 km · 🇮🇳 kn · 🇰🇷 ko · 🇱🇹 lt · 🇱🇻 lv · 🇮🇳 ml · 🇮🇳 mr · 🇲🇾 ms · 🇲🇹 mt · 🇲🇲 my · 🇳🇵 ne · 🇳🇱 nl · 🇳🇴 no · 🇮🇳 or · 🇮🇳 pa · 🇵🇭 phi · 🇵🇱 pl · 🇵🇹 pt · 🇧🇷 pt-BR · 🇷🇴 ro · 🇷🇺 ru · 🇱🇰 si · 🇸🇰 sk · 🇸🇮 sl · 🇷🇸 sr · 🇸🇪 sv · 🇰🇪 sw · 🇮🇳 ta · 🇮🇳 te · 🇹🇭 th · 🇹🇷 tr · 🇺🇦 uk-UA · 🇵🇰 ur · 🇺🇿 uz · 🇻🇳 vi · 🇳🇬 yo · 🇹🇼 zh-TW


概述

所有 OmniRoute 管理 API 路由均被划分为三个保护层级之一。分类是静态的,定义于 src/server/authz/routeGuard.ts 中,并在运行任何其他身份验证分支之前进行评估。

层级

第 1 层 — LOCAL_ONLY

执行方式: isLocalOnlyPath(path) → 回环主机检查 绕过方式: 默认无。当请求携带具有 manage 作用域的有效 API 密钥时,LOCAL_ONLY_MANAGE_SCOPE_BYPASS_PREFIXES 中的路径有一个狭窄的豁免 (参见 Manage 作用域豁免)。

这些路由会生成子进程或执行运行时代码。将它们暴露给 非回环流量,会让获取到有效 JWT 的攻击者(例如 通过 Cloudflared/Ngrok 隧道)触发进程生成——这是一类已知的 CVE (GHSA-fhh6-4qxv-rpqj)。

GHSA-fhh6-4qxv-rpqj 是什么(攻击类型): 管理/代理服务器 暴露了一个会启动子进程的端点(npm install、node、浏览器、 代理、git、tar 等)。如果该端点可从主机外访问——因为 运维人员将 OmniRoute 置于 nginx/Cloudflare/Tailscale 隧道之后且 JWT 泄露,或者身份验证配置错误——攻击者就能把"调用 API"变成"在 主机上运行命令"(远程代码执行)。OmniRoute 通过在每个 可生成进程的路由上、在任何身份验证检查之前无条件地强制执行 回环主机检查来封堵这一漏洞:即使令牌通过隧道泄露,也无法触及进程生成。

完整的 LOCAL_ONLY 集合。 权威来源是 src/server/authz/routeGuard.ts 中的 LOCAL_ONLY_API_PREFIXES / LOCAL_ONLY_API_PATTERNS;下表反映了当前状态。 check-route-guard-membership 门禁会枚举可生成进程前缀下的每个 route.ts, 如果有任何一个未被归类为 local-only,则 CI 失败。

前缀 / 模式 仅限本地的原因
/api/mcp/ MCP 服务器 — 生成 stdio 桥接 + SSE 处理器
/api/cli-tools/runtime/ CLI 工具运行时 — 执行任意插件代码
/api/cli-tools/{omp,letta,grok-build,forge,jcode,qwen}-settings 各工具的设置写入器,可修改主机上的工具二进制文件/配置
/api/cli-tools/{claude,cline,codewhale,codex,crush,deepseek-tui,droid,kilo,openclaw,pi,smelt}-settings 与上述六个同类端点相同的 getCliRuntimeStatus() 进程生成(GHSA-35fw-cv32-2373)
/api/cli-tools/{all-statuses,status,detect} CLI 清单探测 — 对每个工具生成 command -v / --version 进程(GHSA-35fw-cv32-2373)
/api/cli-tools/antigravity-mitm Antigravity MITM 代理控制(生成/指向系统代理)
/api/modality-bridge/video/ 严格的可信回环 Video Bridge 运行时探测与内部提取桥接
/api/services/ 嵌入式服务(9Router / CLIProxy / Bifrost / Mux / Dario)— npm install + 生成进程
/dashboard/providers/services/ 嵌入式服务 UI 的反向代理
/api/tunnels/cloudflared 安装/生成 cloudflared 二进制文件
/api/tunnels/tailscale/{install,enable,disable,login,start-daemon} 在主机上安装/控制 tailscaled
/api/copilot/ 未经身份验证的 LLM 驱动器 — 默认仅限 CLI
/api/tools/agent-bridge/ AgentBridge — 生成 MITM 服务器 + DNS 修改
/api/tools/traffic-inspector/ Traffic Inspector — http-proxy 监听器 + 系统代理
/api/settings/mitm 启用 MITM 拦截(系统级代理状态)
/api/issue-agent/ Issue 代理 — 针对仓库生成本地工具
/api/plugins/, /api/plugins 插件 — 通过 worker_threads + child_process 加载/执行
/api/middleware/ 用户中间件 — 在进程内加载/执行操作者代码
/api/system/version 自动更新(仅 POST;GET/HEAD/OPTIONS 除外)— 生成 git checkout + npm install
/api/db-backups/exportAll 为导出存档生成 tar
/api/local/ 一键本地启动器(目前为 Redis)— 生成 podman/docker
/api/headroom/start, /api/headroom/stop Headroom 代理生命周期 — 生成 python CLI / 向 PID 发送信号
/api/jobs, /api/jobs/ 任务运行器控制 — 执行计划的主机端工作
/api/oauth/cursor/auto-import 导入凭据前执行 execFile("which", ["cursor"])
/api/oauth/kiro/auto-import 从主机读取 Kiro CLI 凭据文件
/api/skills/collect/ 技能收集 — 检测/安装本地工具
/api/skills/install, /api/skills/executions Skill 处理器注册 + 执行 —— 可触达沙箱容器生成(GHSA-jx89)
/api/discovery/ 本地网络/提供者发现探测
/api/vnc-session (VNC_ROUTE_PREFIX) 启动有头浏览器 + VNC 会话,用于交互式登录
/api/acp/agents ACP —— 发现并启动本地 CLI 代理二进制文件
/api/resilience/connections 按账户的弹性 JSON(冷却、熔断、锁定)。仪表板 HTML 并非仅限本地访问。
/api/providers/cursor/agent-availability 仪表板安装提醒检查 —— 会启动 cursor-agent status --format json
/api/providers/{id}/login (regex) 启动有头 Playwright Chromium 以进行 Web Cookie 登录
/api/providers/volcengine-plan/connect (regex) 手动有头流程 + 基于会话的手机/短信自动登录(会启动 Playwright)
/api/providers/{id}/refresh-cursor (regex) 手动 Cursor 会话续期 —— 触发 cursor-agent
/api/providers/{id}/chatgpt-web-codex-doctor (regex) 诊断本地 Codex CLI 安装(会启动该二进制文件)

违规时的响应: 403 LOCAL_ONLY

管理范围(manage scope)豁免

当且仅当请求携带 Authorization: Bearer <api-key> 且其元数据包含 manage 范围(或 admin)时,一部分 LOCAL_ONLY 路径才允许从非回环 地址访问。该豁免通过 LOCAL_ONLY_MANAGE_SCOPE_BYPASS_PREFIXES 按路径 显式控制,因此任何新增 LOCAL_ONLY 路径的默认行为仍是严格仅限回环。未认证的 请求以及使用非 manage 密钥的请求仍会以 403 LOCAL_ONLY 被拒绝。

目前唯一可豁免的前缀是 /api/mcp/。/api/cli-tools/runtime/ 和 /api/services/ 被有意排除在外,因为它们可以生成任意 子进程(npm install、node),而这正是 LOCAL_ONLY 层级 旨在防范的 CVE 类型。

#7895 —— mcp:connect 窄范围: /api/mcp/ 豁免还接受 持有窄范围 mcp:connect 的 Bearer 密钥 (src/shared/constants/managementScopes.ts::MCP_CONNECT_SCOPE), 通过 src/server/authz/policies/management.ts 中的 hasMcpConnectOrManageScope() 进行检查。 该范围仅适用于 /api/mcp/ —— mcp:connect 在任何其他 管理路由上均不授予任何权限(包括未来可能新增的任何其他 LOCAL_ONLY 豁免前缀),并且被有意排除在 MANAGEMENT_API_KEY_SCOPES 之外。持有 manage/admin 的密钥仍与之前 一样通过豁免;mcp:connect 是一种低权限替代方案, 适用于不需要广泛管理访问权限的远程纯 MCP 调用方。

请求 路径 结果
非回环,无 Bearer /api/mcp/* 403 LOCAL_ONLY
非回环,Bearer 具有 manage 范围 /api/mcp/* 允许
非回环,Bearer 具有 mcp:connect 范围 /api/mcp/* 允许
非回环,Bearer 不具有 manage/mcp:connect /api/mcp/* 403 LOCAL_ONLY
非回环,Bearer 具有 mcp:connect 范围 /api/cli-tools/runtime/* 403 LOCAL_ONLY
非回环,Bearer 具有 manage 范围 /api/cli-tools/runtime/* 403 LOCAL_ONLY
回环,任意/无 Bearer 任意 LOCAL_ONLY 允许(门禁通过)

运维人员指南与审计

如果你在反向代理或隧道(nginx、Caddy、Cloudflare Tunnel、Tailscale、Ngrok)之后运行 OmniRoute,回环检查仍然会保护 上述能够生成进程的路由 —— 客户端地址为非回环的请求会在认证 执行之前以 403 LOCAL_ONLY 被拒绝,因此泄露的 JWT 无法触达进程生成。运维人员 仍需承担两项责任:

  • 不要通过将客户端 IP 伪造为 loopback 来"修复" 403。 设置 X-Forwarded-For: 127.0.0.1,或使用将源地址重写为 loopback 的代理, 会重新打开本层级所封堵的那一类 RCE 漏洞。通过代理暴露 dashboard/API —— 绝不要暴露具备 spawn 能力的路由。
  • 将 manage 作用域的绕过保持在最小范围。 只有 /api/mcp/ 可被绕过, 且必须持有 manage 作用域的 API 密钥。SPAWN_CAPABLE_PREFIXES 永远无法 被加入绕过列表 —— zod schema 会拒绝它们,并且 isLocalOnlyBypassableByManageScope 在运行时同样拒绝它们(纵深防御), 这正是 dashboard 中 "cannot be made bypassable"(无法设为可绕过)的含义。同一 拒绝列表还包含少数几个不执行 spawn、但同样绝不能被远程访问的 LOCAL_ONLY 路由 —— /api/oauth/kiro/auto-import(读取主机本地的 Kiro 凭据文件)、/api/discovery/ 和 /api/issue-agent/。运行时 快照加载器也会从已存储的设置行中丢弃此类条目,而旧版 JSON 导入 (/api/settings/import-json)永远不会写入绕过键。 每个 LOCAL_ONLY_API_PREFIXES 条目必须要么位于拒绝列表中,要么经过审查 确认为具备绕过资格(tests/unit/authz/local-only-bypass-spawn-denylist.test.ts)。 /api/providers/ 下带有动态段和静态路径的 spawn 能力路由 (例如 /login、/refresh-cursor)由 src/shared/constants/spawnCapablePrefixes.ts 中基于正则的配套常量 SPAWN_CAPABLE_PATTERNS / SPAWN_CAPABLE_PATTERN_ANCESTORS 覆盖, 而不是由扁平的 SPAWN_CAPABLE_PREFIXES 数组覆盖 —— 若要捕获它们, 扁平数组将不得不覆盖整个 /api/providers/ 前缀,这会过度扩大远程 dashboard 合法用于 provider CRUD 的路由树范围。

审计访问 —— 用于验证没有来自主机外部的请求到达这些路由:

  • 打开 /dashboard/settings/security 上的 Authorization Inventory:它会渲染 实时的 LOCAL_ONLY 前缀列表、哪些前缀可绕过,以及编译期确定的 spawn 能力("cannot be made bypassable")集合。
  • 在你的反向代理/访问日志中 grep 上述前缀与非 loopback 客户端地址的组合。 任何返回了 200 而非 403 LOCAL_ONLY 的此类命中都意味着代理正在掩盖真实的 客户端 IP —— 请修复代理。
  • OmniRoute 日志中针对这些路径之一的 403 LOCAL_ONLY 是防护 按预期工作的表现,而不是需要抑制的错误。

带有调用点探测门禁的远程可达路由

某些路由必须保持对远程 dashboard 可达,但会为本地调用方运行主机侧探测 (which <binary>、<binary> --version、tailscale status --json)。 它们对 GET 而言并非 LOCAL_ONLY;相反,处理器通过 getRequestPeerLocality()(src/shared/utils/apiAuth.ts)读取可信的对端判定, 并且只为 loopback / lan 调用方运行探测。remote 调用方会得到 由非 spawn 来源(持久化状态、设置、文件系统检查)构建的相同响应结构,并标记为 probed: false。

路由 为远程调用方跳过的探测
POST /api/providers/{id}/test、/api/providers/test-batch CLI 运行时探测(allowLocalRuntimeProbe)和 CLI 回退 spawn
GET /api/headroom/status which headroom 以及 python --version 探测
GET /api/tunnels/tailscale、GET /api/tunnels/tailscale/check which tailscale、tailscale status/funnel --json、which brew
GET /api/tunnels/cloudflared(GET 豁免 LOCAL_ONLY) which cloudflared PATH 查找

新增的、会探测主机的读取路由应遵循同样的模式,或者直接归类为 LOCAL_ONLY。

第 2 级 —— ALWAYS_PROTECTED

执行方式: isAlwaysProtectedPath(path) → 跳过 requireLogin=false 绕过 绕过方式: 当 requireLogin=false 时无绕过;始终需要 JWT

这些路由具有破坏性或者不可逆。在"免密码" 安装中放行它们,意味着同一 LAN 上的任何人都可以清空数据库或杀死 服务器进程。

路径 原因
/api/shutdown 终止服务器进程
/api/settings/database 数据库导出、导入和清空
/api/db-backups 完整数据库备份归档的访问
/api/settings/export-json 导出完整的设置数据块(含密钥)
/api/settings/import-json 替换完整的设置数据块
/api/providers/health-autopilot/actions 执行 autopilot 修复操作
/api/settings/obsidian 为任意 vault 根目录签发可复用的 WebDAV 凭据
/api/relay/tokens 签发、列出、编辑和吊销 relay 令牌
/api/cli-tools/apply 重写操作员主机的 CLI 配置
/api/cli-tools/guide-settings 写入由向导驱动的 CLI 配置文件
/api/cli-tools/hermes-agent-settings 写入 Hermes agent 配置
/api/cli-tools/backups 恢复和删除 CLI 配置备份
/api/log-export 将调用日志发送到配置的目标
/api/restart 重启服务器进程

违规时的响应: 401 Authentication required

/api/settings/obsidian 覆盖其 /webdav 子路由:POST 将 WebDAV 文件服务——由 Next.js 之前的自定义 Node 层提供,不在此管道之内——指向调用方选择的根目录,并回显新生成的 Basic 凭据;DELETE 用于轮换这些凭据;父级 POST 则存储 Obsidian REST API 令牌。GHSA-62vw 只是掩盖了 GET 的密码泄露问题;其签发仍位于 fail-open 层级(GHSA-7pq4-8pvv-rx7r)。enableObsidianVaultSync() 还会拒绝等同于数据目录、位于数据目录内部或包含数据目录的 vault。

凭据导出与恢复仅限操作员

上述层级只决定是否需要_某种_凭据。下载已存储凭据的路由在处理器中增加了第二道检查:GET /api/settings/export-json(解密后的提供方连接和原始 API 密钥)、GET /api/db-backups/export(SQLite 文件)以及 GET /api/db-backups/exportAll(完整归档)都会调用 requireOperatorCredential()(src/lib/api/operatorCredential.ts)。它接受仪表盘会话、本地 CLI 主体或已禁用登录的实例,并对任何作用域的 API 密钥(包括 manage 和 admin)以及 oma_ 访问令牌返回 403。与之对应的破坏性操作也遵守同样的边界:POST /api/db-backups/import(上传 .sqlite,包括 dryRun=true)、POST /api/db-backups(按 id 恢复已存储的备份)以及 POST /api/settings/import-json(旧版 JSON 恢复)会替换线上数据库或整个设置/连接/API 密钥集合,因此机器凭据同样无法触发它们。销毁恢复点也遵守同样的边界:DELETE /api/db-backups(清理——保留最新 N 个/丢弃早于 D 天的备份)和 PATCH /api/db-backups(保留策略设置),因为持久化的保留策略正是 cleanupDbBackups() 在每次自动备份后所应用的规则(src/lib/db/backup.ts),所以一个能够保存 keepLatest: 1 的机器凭据将在下一次快照时清空所有备份。列出和创建备份(GET/PUT /api/db-backups)仍使用普通的管理认证。同步令牌签发(POST /api/sync/tokens)使用同一个辅助函数,并额外接受受信任的 loopback 内部服务令牌。

全新安装的引导仅限 loopback——依据真实对端,而非 Host

在未配置管理密码(且无 INITIAL_PASSWORD)时,src/shared/utils/apiAuth.ts 中的 isAuthRequired() 仅对 loopback 对端保持匿名引导开放。loopback 的判定依据受信任的对端信号,按顺序为:带有令牌戳记的真实 TCP 对端(PEER_IP_HEADER + VIA_PROXY_HEADER,即策略所看到的内容)、管道自身的 AUTHZ_HEADER_PEER_LOCALITY 判定(即路由处理器所看到的内容,仅在设置了 OMNIROUTE_PEER_STAMP_TOKEN 时才受信任),或面向直连调用方的真实 socket 对端。Host / nextUrl.hostname 从不参与判定,且首个密码的写入(POST /api/settings/require-login)也处于同样的约束之下,而非对每个网络对端开放(GHSA-7pq4-8pvv-rx7r)。managementPolicy 会显式向下传递自己的 peerContext 判定,因此原始(剥离前)请求的头部永远不会决定该判定。

第 3 层——MANAGEMENT(默认)

所有其他管理路由。除非配置了 requireLogin=false,否则需要认证。CLI 令牌可以认证这些路由(loopback + 有效 HMAC)。

评估顺序

managementPolicy.evaluate(ctx)
  1. isLocalOnlyPath(path)?
     → 回环地址                                  → 继续执行
     → 非回环地址、具有 manage 作用域的 Bearer
        且 isLocalOnlyBypassableByManageScope   → 允许 (management_key)
     → 否则                                      → 拒绝 403 LOCAL_ONLY
  2. isInternalModelSyncRequest(ctx)?
     → 允许 (system)
  3. hasValidCliToken(headers)?
     → 允许 (cli) [回环地址 + timingSafeEqual HMAC 检查]
  4. isAlwaysProtectedPath(path) 或 requireLogin=true?
     → isDashboardSessionAuthenticated?
        → 允许 (dashboard_session)
     → 不可绕过路径上的具有 manage 作用域的 Bearer?
        → 允许 (management_key)
     → 拒绝 401/403
  5. requireLogin=false?
     → 允许 (anonymous)

步骤 1 中的 manage 作用域分支是唯一能够满足 LOCAL_ONLY 路由要求的认证路径;认证后端故障时返回 503(而不是 403),因此数据库过期不会静默降级为“拒绝”。

添加具有进程启动能力的新路由

  1. 将路径前缀添加到 src/server/authz/routeGuard.ts 中的 LOCAL_ONLY_API_PREFIXES
  2. 在 tests/unit/authz/routeGuard.test.ts 中添加测试,断言 isLocalOnlyPath() 对新前缀返回 true
  3. 绝不要跳过此步骤 — 请参阅 CLAUDE.md 中的硬性规则 #15
  4. 决定:此路由是否也应属于 LOCAL_ONLY_MANAGE_SCOPE_BYPASS_PREFIXES? 默认答案是否。仅当该路由可以安全地向 manage 作用域持有者公开时才选择加入(即不会启动任意用户控制的代码)。

添加可由 manage 作用域绕过的路径

  1. 确认该路由不会执行用户提供的代码或命令。如果会,请停止 — 此例外机制并不适用。
  2. 将前缀追加到 src/server/authz/routeGuard.ts 中的 LOCAL_ONLY_MANAGE_SCOPE_BYPASS_PREFIXES
  3. 在 tests/unit/authz/management-policy.test.ts 中为全部四种请求形式添加覆盖:无 Bearer(403)、manage Bearer(允许)、非 manage Bearer(403),以及针对每个前缀的回归测试,确保 /api/cli-tools/runtime/* 即使带有 manage Bearer 也始终严格限制为回环地址。

文件

文件 用途
src/server/authz/routeGuard.ts 常量和辅助函数
src/server/authz/policies/management.ts 评估逻辑
tests/unit/authz/routeGuard.test.ts 层级辅助函数的单元测试
tests/unit/authz/management-policy.test.ts evaluate() 的单元测试

在 OpenAPI 中记录安全层级

向 docs/openapi.yaml 添加新路由时,如果该路由已由 routeGuard.ts 分类,请应用对应的供应商扩展:

routeGuard.ts 分类 YAML 注解 强制执行规则
LOCAL_ONLY_API_PREFIXES x-loopback-only: true 无条件阻止来自非回环地址的访问
ALWAYS_PROTECTED_API_PATHS x-always-protected: true 即使 requireLogin=false 也要求认证
内部管理/调试路由 x-internal: true 默认不在 /dashboard/api-endpoints 中显示
无(公开/标准认证) (无需注解) 由标准 requireLogin 控制的访问

验证

以下两个脚本用于确保 YAML 注解与 routeGuard.ts 之间的一致性:

  • scripts/check/check-openapi-coverage.mjs — 覆盖率低于 99% 时失败
  • scripts/check/check-openapi-security-tiers.mjs — 当 x-loopback-only 或 x-always-protected 注解与编译时常量不一致时失败

这两个脚本都会在预提交钩子和 CI 中运行。

误报规则

如果某个路由带有 x-always-protected 或 x-loopback-only 注解,但并不在 routeGuard.ts 常量中,覆盖率脚本将失败。正确的修复方式始终是让 YAML 与 routeGuard.ts 实际强制执行的规则保持一致 — 而不是在尚未实现相应强制执行逻辑的情况下,将路由添加到 routeGuard.ts。


另请参阅

  • docs/security/CLI_TOKEN.md — CLI 机器 ID 令牌
  • docs/architecture/AUTHZ_GUIDE.md — 完整的授权流程
  • docs/frameworks/MCP-SERVER.md — MCP 服务器传输方式和作用域