← back to lessons

An incomplete-fix gap confirmed in SOURCE is not a finding until the attacker can REACH a VICTIM's resource - an unguessable token + blocked enumeration kills cross-tenant reachability

Date 2026-06-21. Target: Nextcloud server (master, 35.0.0-dev). first_patched-verifier on CVE-2026-45157 (share tokens read share-owner's temp upload files). Result: honest downgrade to WALK. The incomplete-fix gap is REAL in source but NOT exploitable. Banked because it sharpens the Floor's REACHABILITY gate (distinct from disclosure / by-design).

The candidate (real source-level incomplete-fix)

CVE-2026-45157 fix added: UploadHome::getChildren() throws MethodNotAllowed (blocks listing the owner's upload-session ROOT), ChunkingV2Plugin::beforeGet blocks content read via GET, RootCollection disableListing. Set-difference at HEAD found two genuine sibling gaps: - apps/dav/lib/Upload/UploadFolder.php:57 getChildren() - UNGUARDED. UploadHome::getChild($uploadId) (not blocked) -> UploadFolder -> getChildren() lists a NAMED session's chunks via PROPFIND. The fix blocked the ROOT listing but not the per-session listing. - ChunkingV2Plugin hooks GET/PUT/DELETE/MOVE/MKCOL but NOT COPY or PROPFIND - so a COPY content-read of an upload chunk bypasses the GET-only beforeGet guard. Both confirmed in source, not disclosed, present at latest. It LOOKS like a cross-tenant incomplete-fix on a CASH target (Nextcloud HackerOne).

Why it WALKS (the reachability gate, Floor #2)

To exploit cross-tenant you must address a VICTIM's specific upload session, i.e. know its <uploadId>. But: 1. Enumeration is blocked - UploadHome::getChildren() throws (the fix), so you cannot LIST the victim's session ids. 2. The id is unguessable - the uploadId is a CLIENT-chosen folder name created via MKCOL, and Nextcloud's documented + web-UI convention is a RANDOM UUID (myapp-<uuid4>). No server-side predictable component. So the only sessions you can address are ones whose UUID you ALREADY know - which are your OWN (you created them). Listing/reading your own in-flight chunks crosses no boundary. There is no path from "share-token holder" to "a victim's unguessable uploadId."

=> The source gap is a defense-in-depth incompleteness (the fix SHOULD also guard UploadFolder::getChildren + COPY for belt-and-suspenders), but it is NOT a practically exploitable cross-tenant vuln. Honest downgrade: low / informational. A HackerOne submission would be closed "requires knowledge of an unguessable upload token."

Banked rule (reinforces Floor gate #2 - reachability/privilege)

A source-confirmed incomplete-fix has THREE more gates to pass before it is a finding, and REACHABILITY is the one that kills "real-but-unreachable" gaps: - The unguarded sibling must operate on a resource the attacker can ADDRESS as a VICTIM's, not just their own. - If the resource is keyed by an UNGUESSABLE identifier (random UUID / crypto token / secret-derived path) AND there is no enumeration/leak primitive to obtain it, the cross-tenant path is closed - downgrade regardless of how real the missing guard is. - Specifically check: can the attacker ENUMERATE the id (a listing endpoint)? PREDICT it (sequential/timestamp/weak-random)? OBSERVE/LEAK it (logs, error messages, a shared channel, a co-tenant view)? If all three are no -> the gap is defense-in-depth, not exploitable. This is the inverse of the IDOR pattern: IDOR works because the id is guessable/enumerable; when the fix ALSO blocks enumeration and the id is a UUID, the residual sibling-gap is inert. Don't spend a live PoC on it (it would only prove you can read your OWN known-id session).

Contrast (so the gate isn't over-applied)

The Forgejo wrr5 win was reachable because the resource was the TOKEN OWNER'S OWN account (no victim-id needed - you authenticate AS the principal whose private data leaks). And the GitLab iterations IDOR was about ENUMERABLE group descendants. Reachability turns on: is the victim's resource addressable by the attacker? Own-account scope = always reachable; enumerable id = reachable; unguessable-unenumerable id = NOT reachable.

Generated 2026-07-02 13:15:04 UTC | auto-sync /15min