blastoise/todo/ws-guest-control-permission/overview.md

3.4 KiB

Fix WS Control-Permission Bypass for Guests

Priority: High · Effort: Trivial (one location) · Risk: Low

Problem

Guests are intended to be unable to control playback (routes/helpers.ts:30:

if (user.is_guest && permission === "control") return false;

) but the WebSocket message handler does not route through userHasPermission. It re-implements the check inline and omits the guest guard:

// websocket.ts:87-93
const canControl = user.is_admin
  || config.defaultPermissions?.includes("control")
  || hasPermission(userId, "channel", ws.data.channelId, "control");
if (!canControl) { ... return; }

Because the default config ships defaultPermissions: ["listen", "control"] (config.json:5-8), any guest can pause, unpause, seek, and jump on any channel, affecting all listeners.

The HTTP control endpoints (routes/channels.ts:176, 196, 216, 261) correctly use userHasPermission, so this is an inconsistency between the two code paths that reach the same Channel mutators.

Affected Location

  • websocket.ts:86-93 — inline permission check, guest-unaware.

Root Cause

Duplicated permission logic in two places. The WS handler was written before / diverged from helpers.userHasPermission.

Implementation Plan

Step 1 — Import userHasPermission

At the top of websocket.ts:

import { userHasPermission } from "./routes/helpers";

Step 2 — Replace the inline check

Replace websocket.ts:86-93 with:

if (!userHasPermission(user, "channel", ws.data.channelId, "control")) {
  console.log("[WS] User lacks control permission:", user.username, "(guest=" + user.is_guest + ")");
  return;
}

userHasPermission already encapsulates: admin bypass, the guest control denial (helpers.ts:30), the defaultPermissions config check, and the DB permission lookup. Routing through it makes WS and HTTP behavior identical.

Step 3 — Verify no other call sites rely on the old behavior

grep the repo for the inline pattern (config.defaultPermissions?.includes("control")) — it should now appear only in helpers.ts:33 (the canonical check). Any other duplicate should be replaced the same way.

Validation

  • Guest cannot control: sign in as a guest (or hit the server unauthenticated with allowGuests: true), open a channel WS, send {"action":"pause"}. Confirm the channel does not pause and the server logs the "lacks control permission" line.
  • Non-guest user still controls: sign in as test/testuser, send the same action, confirm the channel pauses.
  • Admin controls: sign in as admin, confirm control works on channels the admin does not own.
  • Guest still receives state: confirm the guest client still gets track/state broadcasts (i.e. the listen path is unaffected — only control is denied).
  • Config matrix: temporarily set defaultPermissions to ["listen"] only and confirm a non-guest non-admin user is now also denied control via WS (matches the HTTP path).

Risk / Rollback

  • The only behavioral change is denying control to guests (and, if defaultPermissions lacks "control", to non-admin users) — which is the documented intent. If any deployment currently relies on guests controlling playback, that was an unintentional privilege and should be re-granted explicitly via the permissions table, not by reverting.
  • No schema or DB change.
  • Rollback = revert the single edit.