From 372a19e7c9556cbe1626c792b14e3d85f33e635e Mon Sep 17 00:00:00 2001 From: chenzeyan54-commits Date: Sun, 27 Sep 2026 05:35:14 +0800 Subject: [PATCH] fix(#156): [Bug]: Any participant can start, advance or end someone else's quiz `handleMessage` in `apps/ws-server/src/utils/handleMessages.ts` never verifies that the sender is an organizer before acting on host-only messages. Signed-off-by: chenzeyan54-commits --- apps/ws-server/src/utils/handleMessages.ts | 37 ++++++++++++++++++---- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/apps/ws-server/src/utils/handleMessages.ts b/apps/ws-server/src/utils/handleMessages.ts index c91e0d5..c7b61ba 100644 --- a/apps/ws-server/src/utils/handleMessages.ts +++ b/apps/ws-server/src/utils/handleMessages.ts @@ -92,6 +92,29 @@ export const handleMessage = async (client: Client, data: any) => { const { sessionId, role, participantId, userId } = payload; + // Verify if the client claims to be an organizer + let verifiedRole = role; + if (role === 'ORGANIZER') { + if (!userId) { + verifiedRole = 'PARTICIPANT'; + } else { + const session = await getCachedSession(sessionId); + const quizId = session?.quizId; + const isOrganizer = quizId + ? await prisma.quizOrganizer.findFirst({ + where: { + quizId, + userId, + } + }) + : null; + + if (!isOrganizer) { + verifiedRole = 'PARTICIPANT'; + } + } + } + // A reconnecting client re-joins with the same participantId while // its previous socket may still be registered: the heartbeat can // take up to two rounds to notice a dead peer, and a background @@ -114,11 +137,11 @@ export const handleMessage = async (client: Client, data: any) => { indexClient(client, sessionId); client.sessionId = sessionId; - client.role = role; + client.role = verifiedRole; client.participantId = participantId; client.userId = userId; - if (role === 'ORGANIZER') { + if (verifiedRole === 'ORGANIZER') { const session = await getCachedSession(sessionId); const participants = await getCachedParticipants(sessionId); @@ -136,7 +159,7 @@ export const handleMessage = async (client: Client, data: any) => { console.log("JOIN CODE FROM WS:", payload); - } else if (role === 'PARTICIPANT' && participantId) { + } else if (verifiedRole === 'PARTICIPANT' && participantId) { const participant = await prisma.participant.findUnique({ where: { id: participantId } }); @@ -179,6 +202,7 @@ export const handleMessage = async (client: Client, data: any) => { case 'quiz:start': { + if (client.role !== 'ORGANIZER' || client.sessionId !== payload.sessionId) break; const { sessionId } = payload; await prisma.quizSession.update({ where: { id: sessionId }, @@ -189,6 +213,7 @@ export const handleMessage = async (client: Client, data: any) => { } case 'quiz:next-question': { + if (client.role !== 'ORGANIZER' || client.sessionId !== payload.sessionId) break; const { sessionId, questionIndex = 0 } = payload; const questions = await getCachedQuestions(sessionId); @@ -268,7 +293,7 @@ export const handleMessage = async (client: Client, data: any) => { case 'quiz:end': { - + if (client.role !== 'ORGANIZER' || client.sessionId !== payload.sessionId) break; const { sessionId } = payload; if (!sessionId) break; @@ -293,6 +318,4 @@ export const handleMessage = async (client: Client, data: any) => { } } -} - - +} \ No newline at end of file