[Bug]: Deleting an absent Space id can delete another Space by directory-title alias
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 146
- Forks
- 20
- Avg merge
- 2d 6h
- Merged PRs (30d)
- 43
Description
Existing issues
I searched the open issues for Space deletion, directory deletion, and data loss and did not find the same problem.
What happened?
Deleting a nonexistent Space id can recursively delete a different, valid Space when the requested id matches that Space's title-derived directory name.
Example: a Space has stable id canvas-123 and title/directory AliasVictim/. DELETE /api/canvas/AliasVictim succeeds even though AliasVictim is not a Space id, then GET /api/canvas/canvas-123 returns 404 and the directory is gone.
The same fallback can resolve reserved workspace directories such as setting/, so this is a data-loss boundary rather than only an incorrect status code.
I reproduced the behavior on current PR #83 HEAD and again on its exact merge base 973c22ce, so the bug predates that PR.
Expected behavior
Deleting an absent stable Space id must return a not-found outcome and leave every existing Space and workspace-owned directory unchanged. Any orphan-cleanup path must operate only on a directory whose persisted record proves it belongs to the requested stable id.
Steps to reproduce
- Start Huabu with a disposable workspace.
- Create a Space titled
AliasVictim; record the returned stablecanvas-*id. - Confirm the Space is readable through its stable id.
- Send
DELETE /api/canvas/AliasVictimeven though no Space has that stable id. - Observe a 200 response.
- Fetch the real stable id and inspect the workspace: the request now returns 404 and
AliasVictim/has been recursively removed.
This occurred deterministically in both tested revisions.
Root cause
DiskSpaceRepository.beginDelete() creates a CanvasStore for the unverified requested id because absent ids are allowed to enter orphan cleanup. finish() calls CanvasStore.destroy(). Its canvasRoot(requestedId) lookup calls canvasDirName(), which falls back to the requested id itself when no stable-id index entry exists. destroy() then recursively removes that fallback path.
Relevant locations:
apps/server/src/modules/storage/backends/disk/space-repository.ts(beginDelete/finish)apps/server/src/modules/storage/backends/disk/legacy/canvas-store.ts(destroy)apps/server/src/modules/workspace/disk/canvas-dirs.ts(canvasDirNamefallback)
Regression acceptance criteria
- Deleting an absent id that matches another Space's directory/title reports not found and preserves that Space byte-for-byte.
- A request targeting
settingcannot remove workspace settings. - Legitimate stable-id deletion still removes structured state and composed blobs.
- Any supported orphan cleanup resolves ownership from persisted stable identity rather than an arbitrary fallback directory name.
- Coverage exercises a cold directory index as well as a warm one.
System information
Source checkout on Linux 6.17.0-35-generic x86_64, Node.js v24.16.0, pnpm 10.34.3. Reproduced against PR #83 HEAD 69a6a4e4 and merge base 973c22ce using isolated temporary workspaces.
Sensitive information
This report contains only synthetic Space ids, titles, and temporary-workspace observations; no credentials or private documents are included.
— posted by OpenAI Codex (via the issue-tracker skill)
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with beginDelete and finish in apps/server/src/modules/storage/backends/disk/space-repository.ts, then trace destroy in legacy/canvas-store.ts and the fallback in workspace/disk/canvas-dirs.ts. Reproduce deletion with a title-derived directory, the reserved setting directory, and cold and warm indexes. Done means absent ids return not found, existing data is preserved, legitimate stable-id deletion still works, and orphan cleanup verifies persisted ownership.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- node.js, typescript
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100