fix(audit-2026-06-11/F-FS-002): belt-and-suspenders collection validation on files read/delete

The admin files endpoints hit FsFilesRepo directly (not via
FilesServiceImpl, which validates), so only create/update guarded the
collection — head/get/list/delete built FS paths from an unvalidated
value. Today nothing can store a traversal-shaped collection, but one
future bad migration / restore tool inserting collection='../../etc'
would give the read/delete paths arbitrary host-file reach.

- FsFilesRepo::{head,get,list,delete} now call guard_collection (create
  and update already did).
- The three admin endpoints (list/get/delete) call
  validate_files_collection up front for a clean 422 instead of the
  repo guard surfacing as an opaque 500.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-12 18:26:46 +02:00
parent e676eb9ba7
commit d9fb0e7f85
2 changed files with 14 additions and 6 deletions

View File

@@ -350,6 +350,7 @@ impl FilesRepo for FsFilesRepo {
collection: &str,
id: Uuid,
) -> Result<Option<FileMeta>, FilesRepoError> {
Self::guard_collection(collection)?;
let row: Option<FileRow> = sqlx::query_as(
"SELECT id, collection, name, content_type, size_bytes, \
checksum_sha256, created_at, updated_at \
@@ -369,6 +370,7 @@ impl FilesRepo for FsFilesRepo {
collection: &str,
id: Uuid,
) -> Result<Option<Vec<u8>>, FilesRepoError> {
Self::guard_collection(collection)?;
let row: Option<(String,)> = sqlx::query_as(
"SELECT checksum_sha256 FROM files \
WHERE app_id = $1 AND collection = $2 AND id = $3",
@@ -434,6 +436,7 @@ impl FilesRepo for FsFilesRepo {
collection: &str,
id: Uuid,
) -> Result<Option<FileMeta>, FilesRepoError> {
Self::guard_collection(collection)?;
// SELECT + DELETE in one tx; unlink afterwards (outside the tx).
let mut tx = self.pool.begin().await?;
let row: Option<FileRow> = sqlx::query_as(
@@ -479,6 +482,7 @@ impl FilesRepo for FsFilesRepo {
cursor: Option<&str>,
limit: u32,
) -> Result<FilesListPage, FilesRepoError> {
Self::guard_collection(collection)?;
let limit = if limit == 0 {
FILES_LIST_DEFAULT_LIMIT
} else {