diff --git a/src/lotus/lotusAudioInject.ts b/src/lotus/lotusAudioInject.ts index 259c4d24..998392e0 100644 --- a/src/lotus/lotusAudioInject.ts +++ b/src/lotus/lotusAudioInject.ts @@ -12,6 +12,7 @@ import { type IWidgetApiRequest } from "matrix-widget-api"; import { type CallViewModel } from "../state/CallViewModel/CallViewModel"; import { widget } from "../widget"; import { LotusWidgetActions } from "./lotusActions"; +import { lotusFlag } from "./lotusWidget"; /** Hard cap so a malformed/huge clip can't hold a published track open forever. */ const MAX_CLIP_MS = 30_000; @@ -48,7 +49,11 @@ export function startLotusAudioInject(vm: CallViewModel): () => void { const activeClips = new Set<() => void>(); const handler = (ev: CustomEvent): void => { + // Always ack so the transport doesn't hang, but only act when the host has + // explicitly opted in: audio-inject publishes under the local user's + // identity, so it must not be silently armed for every call. void w.api.transport.reply(ev.detail, {}); + if (!lotusFlag("lotusAudioInject")) return; const data = ev.detail.data as | { url?: unknown; volume?: unknown } | undefined; diff --git a/src/lotus/lotusDecorations.ts b/src/lotus/lotusDecorations.ts index 6dbb8d6d..a08cff75 100644 --- a/src/lotus/lotusDecorations.ts +++ b/src/lotus/lotusDecorations.ts @@ -70,7 +70,11 @@ export function startLotusDecorations(): () => void { | undefined; const next: Record = {}; if (data?.decorations && typeof data.decorations === "object") { - for (const [userId, url] of Object.entries(data.decorations)) { + // Cap the roster so a pathological map can't spawn unbounded overlays. + for (const [userId, url] of Object.entries(data.decorations).slice( + 0, + 512, + )) { const safe = safeImageUrl(url); if (safe) next[userId] = safe; } diff --git a/src/lotus/lotusDenoise.ts b/src/lotus/lotusDenoise.ts index 623c986a..a3f3ed22 100644 --- a/src/lotus/lotusDenoise.ts +++ b/src/lotus/lotusDenoise.ts @@ -31,12 +31,31 @@ import { * survives EC's mid-call reconnect — fixing the A7 "mic dead after reconnect" * bug. Additive: no-op without the flag. */ +/** + * Resolve the denoise asset base, forcing it to be SAME-ORIGIN. The base is fed + * to `audioWorklet.addModule()`, which executes the target as code in the + * worklet scope (processing the live mic) — so a cross-origin base from a + * crafted call link would be arbitrary code execution. Any non-same-origin or + * malformed value falls back to the bundled "./denoise/". + */ +function safeAssetBase(raw: string | null): string { + const fallback = "./denoise/"; + if (!raw) return fallback; + try { + const u = new URL(raw, window.location.href); + if (u.origin !== window.location.origin) return fallback; + return u.href.endsWith("/") ? u.href : `${u.href}/`; + } catch { + return fallback; + } +} + export function startLotusDenoise(vm: CallViewModel): () => void { if (lotusParam("lotusDenoise") !== "ml") return () => undefined; const config: LotusDenoiseConfig = { model: lotusParam("lotusModel") === "speex" ? "speex" : "rnnoise", - assetBase: lotusParam("lotusDenoiseBase") || "./denoise/", + assetBase: safeAssetBase(lotusParam("lotusDenoiseBase")), gate: lotusFlag("lotusGate"), gateThreshold: Number(lotusParam("lotusGateThreshold")) || -50, };