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:
2026-09-06 21:59:39 +02:00
parent 1b7dbad843
commit 82c4261a81
19 changed files with 399 additions and 122 deletions
+20 -2
View File
@@ -29,6 +29,11 @@ export function CallRoom({ slug }: { slug: string }) {
const localStreamRef = useRef<MediaStream | null>(null);
const screenStreamRef = useRef<MediaStream | null>(null);
const sessionRef = useRef<StoredMeetingSession | null>(null);
const remotesRef = useRef<Map<string, RemoteEntry>>(new Map());
useEffect(() => {
remotesRef.current = remotes;
}, [remotes]);
const send = useCallback((message: ClientToServerMessage) => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
@@ -149,9 +154,15 @@ export function CallRoom({ slug }: { slug: string }) {
if (cancelled) return;
const protocol = window.location.protocol === "https:" ? "wss" : "ws";
const ws = new WebSocket(`${protocol}://${window.location.host}/ws/signaling?token=${encodeURIComponent(parsed.token)}`);
const ws = new WebSocket(`${protocol}://${window.location.host}/ws/signaling`);
wsRef.current = ws;
// Token travels as the first message, not a `?token=` query param, so
// it doesn't end up persisted in reverse-proxy/access logs.
ws.onopen = () => {
ws.send(JSON.stringify({ type: "auth", token: parsed.token }));
};
ws.onmessage = (event) => {
const message: ServerToClientMessage = JSON.parse(event.data);
handleServerMessage(message);
@@ -207,11 +218,18 @@ export function CallRoom({ slug }: { slug: string }) {
sessionStorage.removeItem(MEETING_SESSION_KEY);
break;
}
case "auth-error": {
setPhase("error");
setErrorMessage("Die Meeting-Sitzung ist abgelaufen oder ungültig.");
cleanup();
sessionStorage.removeItem(MEETING_SESSION_KEY);
break;
}
}
}
function remotesLookupName(participantId: string): string {
return remotes.get(participantId)?.name ?? "Teilnehmer:in";
return remotesRef.current.get(participantId)?.name ?? "Teilnehmer:in";
}
return () => {