Repository navigation
Security: deleting a shared cloud folder in Files leaves shares, links and file requests l #418
Description
Where
server/src/routes/sources.ts:154-162(delete route) →followDeleteinserver/src/storage/followRefs.ts:27-34, which for a non-home source only removes comments, favourites and tags. It never orphansshares,public_linksorfile_requests.server/src/storage/pathRefs.ts:123-135deletePathRefs(setsorphaned_aton those tables) runs only fromLocalBackend.trashSync(server/src/storage/local.ts:452-462), i.e. only for the home;rowsUnder(pathRefs.ts:55-57) is hard-wired tosource = '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), cleanupremoveOriginal(transfers.ts:689). server/src/storage/shares.ts:131-134ShareBackend.writeforwardsroot + pathto 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-85liveLinkandserver/src/routes/fileRequests.ts:108-115liveRequestcheck onlyorphaned_at/expiry;fileRequests.ts:186-190, 242-243a visitor's cloud upload only asks the provider's free space, thenbackend.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())
- Owner: mkdir
/Taxinconn:<id>, PUT/Tax/note.txt; create an edit share formember, a public link and a file request on/Tax. - Owner:
POST /api/v1/sources/conn:<id>/delete {paths:['/Tax']}→ 204;fake.dropbox.byPath('/Tax')undefined. - Observed: grantee
GET …/share:<id>/list?path=/→ 404 NOT_FOUND; visitorGET /s/<token>/info→ 404; owner/api/v1/shares/outgoing,/api/v1/links/mine,/api/v1/requests→orphaned: false; request visitorGET /r/<token>/info→ 200filesLeft: 100. - grantee
PUT /api/v1/sources/share:<id>/content?path=/planted.txt&onCollision=fail→ 201; owner's Dropbox now holds/Tax/planted.txt. visitorGET /s/<token>/list→ 200 listingplanted.txt;GET /s/<token>/content?path=/planted.txt→ 200 with the grantee's bytes. Request visitor tus create 201 + PATCH 204 →/Tax/visitor.txtin 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_ORPHANEDfor grantees and visitors,orphaned: truein 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.
ShareBackendwrites (write,mkdir,move,copy,delete) and file-request uploads are refused withNOT_FOUNDwhen the shared item is gone, before the provider is asked.
Fix
- Add
orphanPathRefs(db, tables, owner, source, paths)tostorage/pathRefs.ts: for everyPathTablewithsourceColumnandorphanColumn(shares, public_links, file_requests) setorphaned_at = nowwhere owner,source = @source, path at or beneath each deleted path (reuseunder(); parameterise the source instead of the literal'home'). Call it fromfollowDeleteforsource !== 'home'(all cloud delete paths already go throughfollowDelete). - Defence in depth: in
ShareBackend, beforewrite/mkdir/move/copy/delete,await inner.stat(root)and require the row's type (directoryfor anything beneath/); a missing root throwsNOT_FOUNDbefore any provider call. Inroutes/fileRequests.ts,createUploadstatsrow.pathon the cloud target and requires a directory, and the cloudplacementstatslive.pathbeforebackend.write. - 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 → granteelist/content/PUT, visitorinfo/list/content, request visitorinfoand tus creation all 410SHARE_ORPHANED;orphaned: truein/api/v1/shares/outgoing,/api/v1/links/mine,/api/v1/requests;byPath(dir)still undefined after the write attempts. Reverting thefollowDeletechange turns these into 404/201 and the folder reappears.- Same file: after the existing rename-at-provider setup (
sharingMatrix.test.ts:361-378), granteePUT /x.txt→ 404 and request visitor creation → 404;byPath(dir)stays undefined. Reverting theShareBackend/file-request stat makes the PUT 201. - One assertion each for
DuplicateFinder.trashand 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
fix(#418): Security: deleting a shared cloud folder in Files leaves s…
BeSovereign commentedon Oct 20, 2026
Fixed: PR #428 squash-merged into
mainafter green CI (one commit;git revertit to undo).