Skip to content

Security: deleting a shared cloud folder in Files leaves shares, links and file requests l #418

Closed

Description

@BeSovereign

Where

  • server/src/routes/sources.ts:154-162 (delete route) → followDelete in server/src/storage/followRefs.ts:27-34, which for a non-home source only removes comments, favourites and tags. It never orphans shares, public_links or file_requests.
  • server/src/storage/pathRefs.ts:123-135 deletePathRefs (sets orphaned_at on those tables) runs only from LocalBackend.trashSync (server/src/storage/local.ts:452-462), i.e. only for the home; rowsUnder (pathRefs.ts:55-57) is hard-wired to source = 'home'.
  • Same gap on every other cloud delete path: DuplicateFinder.trash (server/src/storage/duplicates.ts:113-114), deleteMovedSources (server/src/storage/transfers.ts:787), cleanup removeOriginal (transfers.ts:689).
  • server/src/storage/shares.ts:131-134 ShareBackend.write forwards root + path to the owner's backend without checking that the shared item still exists; Dropbox/Drive/Graph/pCloud create missing parent folders on upload (the fake too: server/test/support/fakeDropbox.ts:322).
  • server/src/routes/links.ts:79-85 liveLink and server/src/routes/fileRequests.ts:108-115 liveRequest check only orphaned_at/expiry; fileRequests.ts:186-190, 242-243 a visitor's cloud upload only asks the provider's free space, then backend.write(path).

What happens

In the home, deleting a shared item orphans every share, link and request on it (410, shown as orphaned). For a cloud source the same delete through Files leaves all three rows untouched (orphaned: false). They answer 404 while the path is empty and come back to life as soon as anything exists at the path again, and an edit grantee or an anonymous file-request visitor can be the one who recreates it: their write makes the provider create the deleted folder anew in the owner's cloud account and places a file there; the public link on that folder then serves the planted content to anyone with the URL. DECISIONS 2026-10-16 (#252, "Left open") already names the write-recreates-path half as a defect needing its own issue.

Probe (fake Dropbox, startTestApp + fake.dropboxProvider())

  1. Owner: mkdir /Tax in conn:<id>, PUT /Tax/note.txt; create an edit share for member, a public link and a file request on /Tax.
  2. Owner: POST /api/v1/sources/conn:<id>/delete {paths:['/Tax']} → 204; fake.dropbox.byPath('/Tax') undefined.
  3. Observed: grantee GET …/share:<id>/list?path=/ → 404 NOT_FOUND; visitor GET /s/<token>/info → 404; owner /api/v1/shares/outgoing, /api/v1/links/mine, /api/v1/requests → orphaned: false; request visitor GET /r/<token>/info → 200 filesLeft: 100.
  4. grantee PUT /api/v1/sources/share:<id>/content?path=/planted.txt&onCollision=fail → 201; owner's Dropbox now holds /Tax/planted.txt. visitor GET /s/<token>/list → 200 listing planted.txt; GET /s/<token>/content?path=/planted.txt → 200 with the grantee's bytes. Request visitor tus create 201 + PATCH 204 → /Tax/visitor.txt in the owner's Dropbox.

Expected

  • A delete through Files (sources route, duplicate finder, move job, migration cleanup) of a cloud item orphans every share, public link and file request on it or beneath it in the same step, like the home: 410 SHARE_ORPHANED for grantees and visitors, orphaned: true in the owner's lists, never revived by a later item at the path.
  • Whatever made the shared item disappear (deleted in Files, deleted or renamed at the provider): no write through a share or a file request may create its path anew. ShareBackend writes (write, mkdir, move, copy, delete) and file-request uploads are refused with NOT_FOUND when the shared item is gone, before the provider is asked.

Fix

  1. Add orphanPathRefs(db, tables, owner, source, paths) to storage/pathRefs.ts: for every PathTable with sourceColumn and orphanColumn (shares, public_links, file_requests) set orphaned_at = now where owner, source = @source, path at or beneath each deleted path (reuse under(); parameterise the source instead of the literal 'home'). Call it from followDelete for source !== 'home' (all cloud delete paths already go through followDelete).
  2. Defence in depth: in ShareBackend, before write/mkdir/move/copy/delete, await inner.stat(root) and require the row's type (directory for anything beneath /); a missing root throws NOT_FOUND before any provider call. In routes/fileRequests.ts, createUpload stats row.path on the cloud target and requires a directory, and the cloud placement stats live.path before backend.write.
  3. No migration. Update the Sharing test matrix: internal shares and public links on home, Google Drive and Dropbox (file/folder, read/edit) #208/Security: edit share of a single file lets the member write files beside it (keep-both on /) #252 DECISIONS entries.

Tests that must fail without the fix

  • server/test/sharingMatrix.test.ts, describe.each(['drive','dropbox']): owner deletes the shared folder in Files → grantee list/content/PUT, visitor info/list/content, request visitor info and tus creation all 410 SHARE_ORPHANED; orphaned: true in /api/v1/shares/outgoing, /api/v1/links/mine, /api/v1/requests; byPath(dir) still undefined after the write attempts. Reverting the followDelete change turns these into 404/201 and the folder reappears.
  • Same file: after the existing rename-at-provider setup (sharingMatrix.test.ts:361-378), grantee PUT /x.txt → 404 and request visitor creation → 404; byPath(dir) stays undefined. Reverting the ShareBackend/file-request stat makes the PUT 201.
  • One assertion each for DuplicateFinder.trash and a cloud move job orphaning a link beneath the deleted item.
  • Playwright (e2e/cloud-sharing.spec.ts): owner deletes a Dropbox folder that has a public link and a share → "My shares" lists both as no longer available, and /s/<token> shows the unavailable alert, not a listing; without the fix the visitor page shows not-found and, after a member's upload, the folder.

🤖 Generated with Claude Code


Found by the automatic review round (claude-fable-5-1, 24f3fc6); per CLAUDE.md the fix starts with a failing behaviour test.

Activity

  1. added
    rebuildPart of the ground-up rebuild (spec 2026-10-14)
    securitySecurity finding
    on Oct 20, 2026
  2. added a commit that references this issue on Oct 20, 2026
  3. BeSovereign commented on Oct 20, 2026

    @BeSovereign
    ContributorAuthor

    Fixed: PR #428 squash-merged into main after green CI (one commit; git revert it to undo).

    • Suites: typecheck: 1 passed, 0 failed | lint: 1 passed, 0 failed | unit: 1198 passed, 0 failed | e2e: 616 passed, 0 failed
    • Declared tests fail without the production change and pass with it (output in the PR).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    rebuildPart of the ground-up rebuild (spec 2026-10-14)securitySecurity finding

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions