From d76e8922cc60b633c8897945ebf548541651e47d Mon Sep 17 00:00:00 2001 From: Stan Runge Date: Thu, 26 Mar 2026 02:13:01 +0100 Subject: [PATCH] fix: idor checks on all exposed endpoints --- src/routes/api/mappools/[id]/+server.ts | 15 ++++++++++-- src/routes/api/mappools/[id]/slots/+server.ts | 12 +++++++--- .../mappools/[id]/slots/[slotId]/+server.ts | 15 ++++++++++-- src/routes/api/matches/[id]/lobby/+server.ts | 3 ++- src/routes/api/matches/[id]/pick/+server.ts | 24 +++++++++++++++---- src/routes/api/matches/[id]/roll/+server.ts | 24 +++++++++++++++---- .../api/matches/[id]/rolling/+server.ts | 3 ++- src/routes/api/matches/[id]/score/+server.ts | 3 ++- 8 files changed, 81 insertions(+), 18 deletions(-) diff --git a/src/routes/api/mappools/[id]/+server.ts b/src/routes/api/mappools/[id]/+server.ts index 72fdcd0..419ac0b 100644 --- a/src/routes/api/mappools/[id]/+server.ts +++ b/src/routes/api/mappools/[id]/+server.ts @@ -2,11 +2,12 @@ import { json, error } from '@sveltejs/kit'; import { db } from '$lib/server/db'; import { mappool, mappoolSlot } from '$lib/server/db/schema'; import { eq } from 'drizzle-orm'; +import { requireAuth, requireOwnerOrAdmin } from '$lib/server/permissions'; import type { RequestHandler } from './$types'; // Get a mappool with its slots export const GET: RequestHandler = async ({ params, locals }) => { - if (!locals.user) error(401, 'Not logged in'); + requireAuth(locals); const pool = await db.query.mappool.findFirst({ where: eq(mappool.id, params.id) @@ -24,7 +25,17 @@ export const GET: RequestHandler = async ({ params, locals }) => { // Delete a mappool export const DELETE: RequestHandler = async ({ params, locals }) => { - if (!locals.user) error(401, 'Not logged in'); + const pool = await db.query.mappool.findFirst({ + where: eq(mappool.id, params.id) + }); + + if (!pool) error(404, 'Mappool not found'); + + requireOwnerOrAdmin( + locals, + pool.createdBy, + 'Only the mappool creator or an admin can delete this' + ); await db.delete(mappool).where(eq(mappool.id, params.id)); return json({ ok: true }); diff --git a/src/routes/api/mappools/[id]/slots/+server.ts b/src/routes/api/mappools/[id]/slots/+server.ts index 8bb257c..231bd83 100644 --- a/src/routes/api/mappools/[id]/slots/+server.ts +++ b/src/routes/api/mappools/[id]/slots/+server.ts @@ -1,13 +1,19 @@ import { json, error } from '@sveltejs/kit'; import { db } from '$lib/server/db'; -import { mappoolSlot } from '$lib/server/db/schema'; +import { mappool, mappoolSlot } from '$lib/server/db/schema'; import { eq, and } from 'drizzle-orm'; -import { getBeatmap } from '$lib/server/osu/api'; +import { requireOwnerOrAdmin } from '$lib/server/permissions'; import type { RequestHandler } from './$types'; // Add a slot to a mappool export const POST: RequestHandler = async ({ params, request, locals }) => { - if (!locals.user) error(401, 'Not logged in'); + const pool = await db.query.mappool.findFirst({ + where: eq(mappool.id, params.id) + }); + + if (!pool) error(404, 'Mappool not found'); + + requireOwnerOrAdmin(locals, pool.createdBy, 'Only the mappool creator or an admin can add slots'); const { beatmapId, category, mods } = await request.json(); diff --git a/src/routes/api/mappools/[id]/slots/[slotId]/+server.ts b/src/routes/api/mappools/[id]/slots/[slotId]/+server.ts index f53f26f..e0aaba8 100644 --- a/src/routes/api/mappools/[id]/slots/[slotId]/+server.ts +++ b/src/routes/api/mappools/[id]/slots/[slotId]/+server.ts @@ -1,11 +1,22 @@ import { json, error } from '@sveltejs/kit'; import { db } from '$lib/server/db'; -import { mappoolSlot } from '$lib/server/db/schema'; +import { mappool, mappoolSlot } from '$lib/server/db/schema'; import { eq } from 'drizzle-orm'; +import { requireOwnerOrAdmin } from '$lib/server/permissions'; import type { RequestHandler } from './$types'; export const DELETE: RequestHandler = async ({ params, locals }) => { - if (!locals.user) error(401, 'Not logged in'); + const pool = await db.query.mappool.findFirst({ + where: eq(mappool.id, params.id) + }); + + if (!pool) error(404, 'Mappool not found'); + + requireOwnerOrAdmin( + locals, + pool.createdBy, + 'Only the mappool creator or an admin can delete slots' + ); await db.delete(mappoolSlot).where(eq(mappoolSlot.id, params.slotId)); return json({ ok: true }); diff --git a/src/routes/api/matches/[id]/lobby/+server.ts b/src/routes/api/matches/[id]/lobby/+server.ts index 235fb62..6f0534f 100644 --- a/src/routes/api/matches/[id]/lobby/+server.ts +++ b/src/routes/api/matches/[id]/lobby/+server.ts @@ -1,10 +1,11 @@ // src/routes/api/matches/[id]/lobby/+server.ts import { json, error } from '@sveltejs/kit'; import { moveToLobby } from '$lib/server/match/engine'; +import { requireRole } from '$lib/server/permissions'; import type { RequestHandler } from './$types'; export const POST: RequestHandler = async ({ params, request, locals }) => { - if (!locals.user) error(401, 'Not logged in'); + requireRole(locals, 'referee', 'Only referees or admins can move matches to lobby'); const body = await request.json().catch(() => ({})); diff --git a/src/routes/api/matches/[id]/pick/+server.ts b/src/routes/api/matches/[id]/pick/+server.ts index dc13ac7..4dd53b4 100644 --- a/src/routes/api/matches/[id]/pick/+server.ts +++ b/src/routes/api/matches/[id]/pick/+server.ts @@ -1,14 +1,30 @@ // src/routes/api/matches/[id]/pick/+server.ts import { json, error } from '@sveltejs/kit'; import { pickMap } from '$lib/server/match/engine'; +import { requireAuth, hasRole } from '$lib/server/permissions'; +import { getMatchFull } from '$lib/server/match/helpers'; import type { RequestHandler } from './$types'; export const POST: RequestHandler = async ({ params, request, locals }) => { - if (!locals.user) error(401, 'Not logged in'); + const user = requireAuth(locals); - const { participantId, mappoolSlotId } = await request.json(); - if (!participantId || !mappoolSlotId) { - error(400, 'participantId and mappoolSlotId required'); + const { mappoolSlotId } = await request.json(); + if (!mappoolSlotId) { + error(400, 'mappoolSlotId required'); + } + + const match = await getMatchFull(params.id); + + // Find the participant linked to the authenticated user + const participant = match.participants.find((p) => p.players.some((pl) => pl.userId === user.id)); + + if (!participant && !hasRole(user.role, 'referee')) { + error(403, 'You are not a participant in this match'); + } + + const participantId = participant?.id; + if (!participantId) { + error(400, 'Could not resolve participant'); } try { diff --git a/src/routes/api/matches/[id]/roll/+server.ts b/src/routes/api/matches/[id]/roll/+server.ts index d955bba..b49c0ae 100644 --- a/src/routes/api/matches/[id]/roll/+server.ts +++ b/src/routes/api/matches/[id]/roll/+server.ts @@ -1,14 +1,30 @@ // src/routes/api/matches/[id]/roll/+server.ts import { json, error } from '@sveltejs/kit'; import { submitRoll } from '$lib/server/match/engine'; +import { requireAuth, hasRole } from '$lib/server/permissions'; +import { getMatchFull } from '$lib/server/match/helpers'; import type { RequestHandler } from './$types'; export const POST: RequestHandler = async ({ params, request, locals }) => { - if (!locals.user) error(401, 'Not logged in'); + const user = requireAuth(locals); - const { participantId, value } = await request.json(); - if (!participantId || value == null) { - error(400, 'participantId and value required'); + const { value } = await request.json(); + if (value == null) { + error(400, 'value required'); + } + + const match = await getMatchFull(params.id); + + // Find the participant linked to the authenticated user + const participant = match.participants.find((p) => p.players.some((pl) => pl.userId === user.id)); + + if (!participant && !hasRole(user.role, 'referee')) { + error(403, 'You are not a participant in this match'); + } + + const participantId = participant?.id; + if (!participantId) { + error(400, 'Could not resolve participant'); } try { diff --git a/src/routes/api/matches/[id]/rolling/+server.ts b/src/routes/api/matches/[id]/rolling/+server.ts index f0cf605..18a6089 100644 --- a/src/routes/api/matches/[id]/rolling/+server.ts +++ b/src/routes/api/matches/[id]/rolling/+server.ts @@ -1,10 +1,11 @@ // src/routes/api/matches/[id]/rolling/+server.ts import { json, error } from '@sveltejs/kit'; import { moveToRolling } from '$lib/server/match/engine'; +import { requireRole } from '$lib/server/permissions'; import type { RequestHandler } from './$types'; export const POST: RequestHandler = async ({ params, locals }) => { - if (!locals.user) error(401, 'Not logged in'); + requireRole(locals, 'referee', 'Only referees or admins can move matches to rolling'); try { const m = await moveToRolling(params.id); diff --git a/src/routes/api/matches/[id]/score/+server.ts b/src/routes/api/matches/[id]/score/+server.ts index 0525002..bf95e7a 100644 --- a/src/routes/api/matches/[id]/score/+server.ts +++ b/src/routes/api/matches/[id]/score/+server.ts @@ -1,10 +1,11 @@ // src/routes/api/matches/[id]/score/+server.ts import { json, error } from '@sveltejs/kit'; import { submitGameScores } from '$lib/server/match/engine'; +import { requireRole } from '$lib/server/permissions'; import type { RequestHandler } from './$types'; export const POST: RequestHandler = async ({ request, locals }) => { - if (!locals.user) error(401, 'Not logged in'); + requireRole(locals, 'referee', 'Only referees or admins can submit scores'); const { matchGameId, scores } = await request.json(); if (!matchGameId || !scores?.length) {