fix: stop book params from setting cover_image_key and confine storage keys #20

Merged
jfro merged 3 commits from jtk/cover-key-traversal into main 2026-10-18 17:10:28 +00:00
Owner

Fixes #7 (high): cover_image_key was cast from book params, so a user could set book[cover_image_key]=../../../<file> on create/update, then delete the book or send remove_cover=true, and LocalBackend.delete/1 would File.rm that path (the same key also fed exists?/retrieve_url).

Independent of #18/#19 (branched from main).

Changes

  • BookController: cover_image_key is dropped from request params (create, update, add-from-lookup, scan, batch add) and logged at warning if sent. Only the server sets it after storing an upload or download.
  • Book.changeset: cover_image_key must look like covers/<uuid>.(jpg|png|gif|webp), the only shape Storage.store_cover produces. Internal callers (sync, the ebook worker) keep working; existing rows aren't revalidated.
  • LocalBackend: keys go through Path.safe_relative/1; absolute keys, empty/nil keys and any .. that leaves base_path return {:error, :invalid_key} (or false from exists?) and log a warning, instead of touching the filesystem. Pages with a bad stored key show no cover rather than crashing.

Tests

test/fuzzy_catalog/storage/local_backend_test.exs (traversal, absolute, empty and nil keys refused for store/delete/exists?/retrieve_url; a file outside uploads survives), test/fuzzy_catalog/catalog/book_test.exs (key format), and controller tests that send a traversal key on create and update. mix precommit: 167 tests pass.

Fixes #7 (high): `cover_image_key` was cast from book params, so a user could set `book[cover_image_key]=../../../<file>` on create/update, then delete the book or send `remove_cover=true`, and `LocalBackend.delete/1` would `File.rm` that path (the same key also fed `exists?`/`retrieve_url`). Independent of #18/#19 (branched from main). ## Changes - **BookController**: `cover_image_key` is dropped from request params (create, update, add-from-lookup, scan, batch add) and logged at warning if sent. Only the server sets it after storing an upload or download. - **Book.changeset**: `cover_image_key` must look like `covers/<uuid>.(jpg|png|gif|webp)`, the only shape `Storage.store_cover` produces. Internal callers (sync, the ebook worker) keep working; existing rows aren't revalidated. - **LocalBackend**: keys go through `Path.safe_relative/1`; absolute keys, empty/nil keys and any `..` that leaves `base_path` return `{:error, :invalid_key}` (or `false` from `exists?`) and log a warning, instead of touching the filesystem. Pages with a bad stored key show no cover rather than crashing. ## Tests `test/fuzzy_catalog/storage/local_backend_test.exs` (traversal, absolute, empty and nil keys refused for store/delete/exists?/retrieve_url; a file outside uploads survives), `test/fuzzy_catalog/catalog/book_test.exs` (key format), and controller tests that send a traversal key on create and update. `mix precommit`: 167 tests pass.
fix: stop book params from setting cover_image_key and confine storage keys
All checks were successful
CI / test (pull_request) Successful in 34s
7240fdff08
A user could send book[cover_image_key]=../../../<file> on create or
update, then delete the book or remove its cover, and LocalBackend would
File.rm that path.

- BookController drops cover_image_key from request params; only the
  server sets it after storing an upload or download
- Book.changeset only accepts keys shaped like covers/<uuid>.<image ext>
- LocalBackend refuses absolute keys and any .. that leaves base_path
  (Path.safe_relative/1) for store, delete, exists? and retrieve_url

Fixes #7
Merge remote-tracking branch 'origin/main' into jtk/cover-key-traversal
All checks were successful
CI / test (pull_request) Successful in 35s
5727a04dca
# Conflicts:
#	test/fuzzy_catalog_web/controllers/book_controller_test.exs
jfro left a comment

Review by Nye (PR review / audit) — APPROVE @5727a04 (originally @7240fdf)

Closes #7: controller drops cover_image_key from params on create/update and add-from-lookup; Book.changeset validate_format only accepts covers/<uuid>.(jpg|png|gif|webp) (validate_* runs only on changes, so legacy rows still save); LocalBackend uses Path.safe_relative for store/delete/exists?. Every server path that writes keys (store_cover, download_and_store_cover, upload, sync, #17's ebook thumbnails) produces exactly that format, so no regressions. Tests cover changeset, backend and controller. Re-checked after main was merged in: the lib/ diff is identical to 7240fdf, #19's test is kept, CI green.

Merge note: will conflict with #17 in book.ex: keep both (#17's rating validation and this PR's cover_image_key handling).

**Review by Nye (PR review / audit) — APPROVE** @5727a04 (originally @7240fdf) Closes #7: controller drops `cover_image_key` from params on create/update and add-from-lookup; `Book.changeset` `validate_format` only accepts `covers/<uuid>.(jpg|png|gif|webp)` (validate_* runs only on changes, so legacy rows still save); LocalBackend uses `Path.safe_relative` for store/delete/exists?. Every server path that writes keys (store_cover, download_and_store_cover, upload, sync, #17's ebook thumbnails) produces exactly that format, so no regressions. Tests cover changeset, backend and controller. Re-checked after main was merged in: the lib/ diff is identical to 7240fdf, #19's test is kept, CI green. Merge note: will conflict with #17 in book.ex: keep both (#17's rating validation and this PR's cover_image_key handling).
Merge remote-tracking branch 'origin/main' into jtk/cover-key-traversal
All checks were successful
CI / test (pull_request) Successful in 54s
da1831802e
# Conflicts:
#	lib/fuzzy_catalog/catalog/book.ex
jfro merged commit 9e1c3b9018 into main 2026-10-18 17:10:28 +00:00
jfro deleted branch jtk/cover-key-traversal 2026-10-18 17:10:28 +00:00
Sign in to join this conversation.
No reviewers
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
jfro/fuzzy_catalog!20
No description provided.