POST /api/media/delete with a page deletes any file in the bundle, the page's own _index.md included #269

Closed
opened 2026-10-13 20:32:14 +02:00 by dominik.polakovics · 0 comments

This was generated by AI during triage.

Summary

POST /api/media/delete with a bundle reference ({ref, page}) removes any
regular file in the page's bundle directory that passes the traversal guard:
the page's own _index.md, a translation _index.de.md, a stray notes.txt,
a dotfile. None of these is a media resource, and .md files are pages and
translations whose own operations handle links and translation state; this
route does none of that and only takes a media delete snapshot.

Desired: a bundle delete removes a file only when the bundle listing
(GET /api/media/bundle) would show it. Any other name answers
404 library.refNotFound {ref}, leaves the file untouched and takes no
snapshot. Library references are unaffected.

Reproduced during triage with a server test: deletes of _index.md,
_index.de.md and notes.txt against services/_index.md all answered
200 {} and removed the files.

Decisions

  • The deletable set is exactly the listing's set: a regular file directly
    in the bundle directory, not a .md file, whose leading bytes sniff as an
    allowed type. Reason: the listing is the editor's view of the bundle's media
    and the route's contract is a media delete; everything else in a bundle has
    an owner elsewhere (page delete, translation delete) or no business being
    touched through this route.
  • The refusal reuses 404 library.refNotFound, the code a missing file
    already gets. Reason: the route resolves media references; a name that is not
    a media resource of the bundle is not a reference it knows, and reusing the
    code needs no new registry entry or client catalog string (ADR 0020) for a
    state the editor never produces.
  • One owner for "is this bundle entry a media resource" (CLAUDE.md, every
    rule has one owner). The predicate the listing in internal/server/media.go
    applies inline today (skip directories, skip .md, sniff the type) becomes
    one named function that both the listing and the delete call. Its home is
    the agent's call; internal/media beside BundleRef and Delete is the
    natural one. No "same as the listing" comment anywhere.
  • The thumb route stays as it is: it already answers 404 for a bundle file
    that does not decode and serves no bytes.

Acceptance criteria

  • With a bundle page, a delete of _index.md, of _index.<lang>.md, of a
    non-media file and of a dotfile each answers 404 library.refNotFound
    with args.ref, the file is still on disk, and no WIP snapshot was taken.
  • A delete of a sniffed image or PDF in the bundle still answers 200 {}
    and removes the file (the existing bundle delete test keeps passing).
  • A test shows the two routes agree: every ref the listing returns can be
    deleted, and a file the listing omits cannot.
  • The .md/sniff rule is evaluated in one function; the listing and the
    delete both call it and the server has no second copy.
  • docs/api.md, section POST /api/media/delete: states that a bundle
    reference must name a file the bundle listing shows, and the 404 line
    covers that case. GET /api/media/bundle needs no change.
  • go test ./... and go vet ./... pass, including the route docs test in
    internal/server.

Out of scope

  • Blocking bundle deletes on references (documented as deliberate).
  • A new error code, or changes to the thumb or refs routes.
  • Deleting pages or translations through this route; they have their own
    operations.

Verification

go test ./internal/media ./internal/server
go test ./... && go vet ./...

Original report

Found while verifying docs/api.md for #264.

handleMediaDelete (internal/server/media.go, bundle branch) accepts any bare filename as a bundle reference (media.IsBundleRef: non-empty, no /) and calls media.Delete(dir, ref), which only guards against traversal. So

POST /api/media/delete {"ref": "_index.md", "page": "services/_index.md"}

removes the page's own content file (and _index.de.md would remove a translation) outside the page operations: no translation handling, no link rewriting, only a media delete snapshot. GET /api/media/bundle never lists .md files, so the editor does not offer this, but the route accepts it.

Expected: a bundle delete refuses a reference that is not a bundle resource the bundle listing would show (at least every .md file), with a coded 400/404.

docs/api.md documents the route as the code behaves today.

> *This was generated by AI during triage.* ## Summary `POST /api/media/delete` with a bundle reference (`{ref, page}`) removes any regular file in the page's bundle directory that passes the traversal guard: the page's own `_index.md`, a translation `_index.de.md`, a stray `notes.txt`, a dotfile. None of these is a media resource, and `.md` files are pages and translations whose own operations handle links and translation state; this route does none of that and only takes a `media delete` snapshot. Desired: a bundle delete removes a file only when the bundle listing (`GET /api/media/bundle`) would show it. Any other name answers `404 library.refNotFound {ref}`, leaves the file untouched and takes no snapshot. Library references are unaffected. Reproduced during triage with a server test: deletes of `_index.md`, `_index.de.md` and `notes.txt` against `services/_index.md` all answered `200 {}` and removed the files. ## Decisions - **The deletable set is exactly the listing's set**: a regular file directly in the bundle directory, not a `.md` file, whose leading bytes sniff as an allowed type. Reason: the listing is the editor's view of the bundle's media and the route's contract is a media delete; everything else in a bundle has an owner elsewhere (page delete, translation delete) or no business being touched through this route. - **The refusal reuses `404 library.refNotFound`**, the code a missing file already gets. Reason: the route resolves media references; a name that is not a media resource of the bundle is not a reference it knows, and reusing the code needs no new registry entry or client catalog string (ADR 0020) for a state the editor never produces. - **One owner for "is this bundle entry a media resource"** (CLAUDE.md, every rule has one owner). The predicate the listing in `internal/server/media.go` applies inline today (skip directories, skip `.md`, sniff the type) becomes one named function that both the listing and the delete call. Its home is the agent's call; `internal/media` beside `BundleRef` and `Delete` is the natural one. No "same as the listing" comment anywhere. - **The thumb route stays as it is**: it already answers 404 for a bundle file that does not decode and serves no bytes. ## Acceptance criteria - With a bundle page, a delete of `_index.md`, of `_index.<lang>.md`, of a non-media file and of a dotfile each answers `404 library.refNotFound` with `args.ref`, the file is still on disk, and no WIP snapshot was taken. - A delete of a sniffed image or PDF in the bundle still answers `200 {}` and removes the file (the existing bundle delete test keeps passing). - A test shows the two routes agree: every ref the listing returns can be deleted, and a file the listing omits cannot. - The `.md`/sniff rule is evaluated in one function; the listing and the delete both call it and the server has no second copy. - `docs/api.md`, section `POST /api/media/delete`: states that a bundle reference must name a file the bundle listing shows, and the 404 line covers that case. `GET /api/media/bundle` needs no change. - `go test ./...` and `go vet ./...` pass, including the route docs test in `internal/server`. ## Out of scope - Blocking bundle deletes on references (documented as deliberate). - A new error code, or changes to the thumb or refs routes. - Deleting pages or translations through this route; they have their own operations. ## Verification go test ./internal/media ./internal/server go test ./... && go vet ./... ## Original report Found while verifying docs/api.md for #264. `handleMediaDelete` (internal/server/media.go, bundle branch) accepts any bare filename as a bundle reference (`media.IsBundleRef`: non-empty, no `/`) and calls `media.Delete(dir, ref)`, which only guards against traversal. So POST /api/media/delete {"ref": "_index.md", "page": "services/_index.md"} removes the page's own content file (and `_index.de.md` would remove a translation) outside the page operations: no translation handling, no link rewriting, only a `media delete` snapshot. `GET /api/media/bundle` never lists `.md` files, so the editor does not offer this, but the route accepts it. Expected: a bundle delete refuses a reference that is not a bundle resource the bundle listing would show (at least every `.md` file), with a coded 400/404. docs/api.md documents the route as the code behaves today.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/deckle#269
No description provided.