From 92bb9e1a12f77ce5e8c1286716198362b2bfdff2 Mon Sep 17 00:00:00 2001 From: Marian Date: Sat, 16 May 2026 18:20:41 +0000 Subject: [PATCH] fix(api/v1): scope category lookup to event_owned or global MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR review pointed out the original lookup db('photo_categories').where({ id: parsedCategoryId }).first() accepted any category id — including one that belongs to a different event. photo_categories carries both event_id (per-event) and is_global (see backend/migrations/legacy/004_add_categories_and_cms.js); the v1 upload route should require either match. Not a privilege issue (apiTokenAuth.js inherits the admin's powers, no per-event scoping), but it lets a misconfigured uploader silently file photos under a category the target event doesn't own — and the 201 echo includes a category_id that makes no semantic sense. Tighten to: .where({ id: parsedCategoryId }) .andWhere(function () { this.where({ event_id: event.id }).orWhere('is_global', true); }) …and update the 400 message to "Unknown or out-of-scope category_id N". OpenAPI description already documents the intended scope. Tests deferred to a follow-up; v1 has no jest harness today, see PR discussion. Co-Authored-By: Claude Opus 4.7 (1M context) --- backend/src/routes/v1/events.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/backend/src/routes/v1/events.js b/backend/src/routes/v1/events.js index 2de46fd6..78c68f94 100644 --- a/backend/src/routes/v1/events.js +++ b/backend/src/routes/v1/events.js @@ -385,9 +385,22 @@ router.post( let categoryId = null; let photoType = 'individual'; if (!Number.isNaN(parsedCategoryId)) { - const category = await db('photo_categories').where({ id: parsedCategoryId }).first(); + // Scope to categories owned by this event (event_id = event.id) or + // marked global (is_global = true) — see migration + // backend/migrations/legacy/004_add_categories_and_cms.js. An API + // token inherits its owning admin's powers (no per-event scoping + // in apiTokenAuth), so accepting any category_id would silently + // mis-file uploads under a category belonging to a different event. + const category = await db('photo_categories') + .where({ id: parsedCategoryId }) + .andWhere(function () { + this.where({ event_id: event.id }).orWhere('is_global', true); + }) + .first(); if (!category) { - return res.status(400).json({ error: `Unknown category_id ${parsedCategoryId}` }); + return res.status(400).json({ + error: `Unknown or out-of-scope category_id ${parsedCategoryId}`, + }); } categoryId = category.id; if (category.slug === 'collage' || category.slug === 'collages') {