openvibers/openvibe.live Commits

Security: stop GET /api/streams returning live ingest keys to anonymous callers

OP OpenVibers committed 16 Sept 2026 · addfd47124 · 1 file, +8 −1

getLiveStreams() selects s.* from streams plus ms.stream_key as managed_stream_key, and the
public stream list spread those rows straight into its response. The endpoint is optionalAuth,
so any unauthenticated request returned the live ingest key of every channel that was
broadcasting at that moment — enough for someone to publish to their slot.

Every sibling handler already redacts both fields before responding (the single-stream route,
the star spotlight and the discover rail all do). This one did not. It does now.

Found while auditing the home page's request path for performance.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMxJm1dB8rdsaMSgNjVa82

Changes

ddiff --git a/server/streaming/routes.js b/server/streaming/routes.js
iindex f3671a83..97a255b4 100644
---- a/server/streaming/routes.js
++++ b/server/streaming/routes.js
@@@ -1060,12 +1060,19 @@ router.get('/', optionalAuth, (req, res) => {
const ext = restreamManager.getExternalViewerCountsForUser(s.user_id, slotId);
const rsVc = robotStreamerService.getRsViewerCount(s.user_id, slotId);
const externalTotal = ext.total + rsVc;
-- return {
++ // getLiveStreams() selects s.* plus ms.stream_key, and this endpoint is public
++ // (optionalAuth, no user required). Every sibling handler redacts the ingest keys
++ // before responding; this one did not, so an anonymous GET returned the live ingest
++ // key of every broadcasting channel — enough to take over their stream.
++ const out = {
...s,
channel: channel || null,
external_viewer_count: externalTotal,
total_viewer_count: (s.viewer_count || 0) + externalTotal,
};
++ delete out.stream_key;
++ delete out.managed_stream_key;
++ return out;
});
res.json({ streams: enriched });
} catch (err) {