fix(audit): M24 — reserve 'branding'/'avatar' file categories from the upload/update API

The public file-stream gate keys off files.category==='branding'; the API
upload/update schemas now reject the reserved categories so a user can't
self-set branding to publicly expose their own file. System writers (admin
image, avatar) set them via the service directly and are unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-02 13:18:24 +02:00
parent fd69a75980
commit 7b74e2314b
2 changed files with 67 additions and 4 deletions

View File

@@ -173,6 +173,23 @@ export async function getPreviewUrl(id: string, portId: string) {
// ─── Update ───────────────────────────────────────────────────────────────────
/**
* Categories that gate system surfaces and must never be settable through the
* general (validated) update path. `branding` is the public-stream gate on
* `/api/public/files/[id]`; `avatar` is system-managed. The upload/update Zod
* schemas (`userFileCategorySchema`) already reject these, so this is
* belt-and-suspenders for any future non-HTTP caller (M24).
*
* NB: `uploadFile` is intentionally NOT guarded — the admin branding writer
* (`admin/settings/image` route) and the avatar writer (`me/avatar` route)
* legitimately call the service with `category: 'branding' | 'avatar'`,
* constructing the payload inline and bypassing the Zod schema. Guarding the
* upload service would break those legitimate system writers. `updateFile`,
* by contrast, has a single schema-validated caller and no legitimate
* reserved-category use, so it is safe to harden here.
*/
const RESERVED_FILE_CATEGORIES = new Set(['branding', 'avatar']);
export async function updateFile(
id: string,
portId: string,
@@ -181,6 +198,10 @@ export async function updateFile(
) {
const existing = await getFileById(id, portId);
if (data.category !== undefined && RESERVED_FILE_CATEGORIES.has(data.category)) {
throw new ValidationError(`Category '${data.category}' is reserved and cannot be set`);
}
const updates: { filename?: string; category?: string } = {};
if (data.filename !== undefined) updates.filename = sanitizeFilename(data.filename);
if (data.category !== undefined) updates.category = data.category;