Fix security and correctness issues found in full-codebase audit
- Restrict meetingPassword field so it isn't leaked via the public REST/GraphQL API - Fix booking slot computation to use Europe/Berlin time instead of the server's UTC clock - Reset email verification when a customer changes their email address - Fix ICS calendar injection via unescaped carriage returns in event descriptions - Bind WebRTC meeting kicks to a persistent client id so they survive a rejoin - Move the meeting signaling token out of the WebSocket query string into the first message - Prevent duplicate signaling connections from leaking when the same participant reconnects - Use the rightmost X-Forwarded-For entry for rate limiting (single trusted reverse proxy) - Use constant-time comparison for the cron endpoint's bearer token - Remove SVG from allowed Media upload types (stored-XSS risk, unused anyway) - Add baseline security headers and a CSP to next.config.ts - Fix CI/CD script injection via unsanitized git ref names in release.yml
This commit is contained in:
+19
-3
@@ -4,6 +4,7 @@ import type { MeetingRole } from "./token";
|
||||
export type Participant = {
|
||||
ws: WebSocket;
|
||||
participantId: string;
|
||||
clientId?: string;
|
||||
name: string;
|
||||
role: MeetingRole;
|
||||
};
|
||||
@@ -11,6 +12,11 @@ export type Participant = {
|
||||
type Room = {
|
||||
participants: Map<string, Participant>;
|
||||
kickedParticipantIds: Set<string>;
|
||||
// Keyed by the client-declared, localStorage-persisted clientId (see
|
||||
// token.ts) rather than the per-connection participantId, so a kick
|
||||
// survives the participant simply reconnecting (which always mints a
|
||||
// fresh participantId) — see isKicked below.
|
||||
kickedClientIds: Set<string>;
|
||||
};
|
||||
|
||||
// In-memory only — this server process is the single source of truth for
|
||||
@@ -22,19 +28,27 @@ const rooms = new Map<string, Room>();
|
||||
function getOrCreateRoom(eventSlug: string): Room {
|
||||
let room = rooms.get(eventSlug);
|
||||
if (!room) {
|
||||
room = { participants: new Map(), kickedParticipantIds: new Set() };
|
||||
room = { participants: new Map(), kickedParticipantIds: new Set(), kickedClientIds: new Set() };
|
||||
rooms.set(eventSlug, room);
|
||||
}
|
||||
return room;
|
||||
}
|
||||
|
||||
export function isKicked(eventSlug: string, participantId: string): boolean {
|
||||
return rooms.get(eventSlug)?.kickedParticipantIds.has(participantId) ?? false;
|
||||
export function isKicked(eventSlug: string, participantId: string, clientId?: string): boolean {
|
||||
const room = rooms.get(eventSlug);
|
||||
if (!room) return false;
|
||||
if (room.kickedParticipantIds.has(participantId)) return true;
|
||||
return clientId ? room.kickedClientIds.has(clientId) : false;
|
||||
}
|
||||
|
||||
export function joinRoom(eventSlug: string, participant: Participant): Participant[] {
|
||||
const room = getOrCreateRoom(eventSlug);
|
||||
const existing = [...room.participants.values()];
|
||||
// Two connections claiming the same participantId (e.g. two tabs replaying
|
||||
// the same stored session) would otherwise silently overwrite the map
|
||||
// entry here and orphan the first socket — close it first so it doesn't
|
||||
// leak as an unreachable broadcast target.
|
||||
room.participants.get(participant.participantId)?.ws.close(4002, "replaced-by-new-connection");
|
||||
room.participants.set(participant.participantId, participant);
|
||||
return existing;
|
||||
}
|
||||
@@ -50,6 +64,8 @@ export function kickFromRoom(eventSlug: string, participantId: string) {
|
||||
const room = rooms.get(eventSlug);
|
||||
if (!room) return;
|
||||
room.kickedParticipantIds.add(participantId);
|
||||
const target = room.participants.get(participantId);
|
||||
if (target?.clientId) room.kickedClientIds.add(target.clientId);
|
||||
room.participants.delete(participantId);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user