Skip to content

[Bug]: Any participant can start, advance or end someone else's quiz #156

Description

@TheCodeHeist-Coder

What happened?

handleMessage in apps/ws-server/src/utils/handleMessages.ts never verifies
that the sender is an organizer before acting on host-only messages.

On join, the client's role is taken directly from its own payload:

const { sessionId, role, participantId, userId } = payload;
// ...
client.role = role;   // whatever the client claimed

Nothing checks this against the QuizOrganizer table. Then quiz:start,
quiz:next-question and quiz:end act on payload.sessionId without looking
at client.role at all.

Impact. Any participant in a live quiz — or anyone who knows a session id —
can:

  • start a quiz before the host is ready (quiz:start)
  • skip to any question, or jump to the end (quiz:next-question)
  • end the quiz for everyone mid-question (quiz:end)

quiz:next-question also accepts an arbitrary questionIndex, so a participant
can request the next question early and see it before others.

Rexial is deployed at rexial.in, so this is exploitable now with a browser
console:

ws.send(JSON.stringify({ type: 'quiz:end', payload: { sessionId: '<id>' } }))

Why the current design cannot catch it. The else if (role === 'PARTICIPANT' && participantId) branch only decides which sync message to send back. It is
not an authorization check, and no later handler revisits the question.

Steps to reproduce

Suggested fix. Two parts, and the first matters most:

  1. On join with role: 'ORGANIZER', verify it. The client should send its
    auth token, and the server should confirm a quizOrganizer row exists for
    that user and quiz with inviteStatus: 'ACCEPTED' — the same check
    generateAccessCodeController already does in apps/http-server. Set
    client.role from the database result, never from the payload.
  2. In quiz:start, quiz:next-question and quiz:end, reject the message
    unless client.role === 'ORGANIZER'.

Please also use client.sessionId (set at join) rather than
payload.sessionId, so a client cannot act on a session it never joined.

Definition of done:

  • A client claiming role: 'ORGANIZER' without a valid organizer token is
    treated as a participant
  • quiz:start, quiz:next-question, quiz:end are no-ops for participants
  • A test covers at least the "participant sends quiz:end" case

Discuss the approach in a comment before starting — this touches the join
handshake, so it needs a matching frontend change in LiveQuiz.tsx.

Which part of Rexial?

ws-server (WebSockets)

How are you running Rexial?

Docker Compose (docker-compose up)

Logs or error output

Environment

No response

Before submitting

  • I searched existing issues and this has not been reported
  • I am not including any secrets, API keys or passwords in this report

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinghelp wantedExtra attention is needed

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions