From 6545ecf11accfbcb98bcee8aa4cc7daa7ce94f7d Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Tue, 4 Mar 2025 00:12:43 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=92=EF=B8=8F(frontend)=20implement=20s?= =?UTF-8?q?trict=20validation=20for=20user-provided=20metadata?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add comprehensive validation for metadata that can be input by users with LiveKit access tokens. Handle all user-controlled metadata with extra care, implementing strict checks to prevent injection attacks or other security issues from malicious input. --- .../notifications/MainNotificationToast.tsx | 7 +++--- .../rooms/livekit/api/lowerHandParticipant.ts | 3 ++- .../Participants/ParticipantsList.tsx | 3 ++- .../rooms/utils/safeParseMetadata.tsx | 22 +++++++++++++++++++ 4 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 src/frontend/src/features/rooms/utils/safeParseMetadata.tsx diff --git a/src/frontend/src/features/notifications/MainNotificationToast.tsx b/src/frontend/src/features/notifications/MainNotificationToast.tsx index a67e841b..7aeaf20f 100644 --- a/src/frontend/src/features/notifications/MainNotificationToast.tsx +++ b/src/frontend/src/features/notifications/MainNotificationToast.tsx @@ -18,6 +18,7 @@ import { ANIMATION_DURATION, ReactionPortals, } from '@/features/rooms/livekit/components/ReactionPortal' +import { safeParseMetadata } from '@/features/rooms/utils/safeParseMetadata' export const MainNotificationToast = () => { const room = useRoomContext() @@ -145,10 +146,10 @@ export const MainNotificationToast = () => { if (isMobileBrowser()) return if (participant.isLocal) return - const prevMetadata = JSON.parse(prevMetadataStr || '{}') - const metadata = JSON.parse(participant.metadata || '{}') + const prevMetadata = safeParseMetadata(prevMetadataStr) + const metadata = safeParseMetadata(participant.metadata) - if (prevMetadata.raised == metadata.raised) return + if (prevMetadata?.raised == metadata?.raised) return const existingToast = toastQueue.visibleToasts.find( (toast) => diff --git a/src/frontend/src/features/rooms/livekit/api/lowerHandParticipant.ts b/src/frontend/src/features/rooms/livekit/api/lowerHandParticipant.ts index e9d91fe4..7c9be819 100644 --- a/src/frontend/src/features/rooms/livekit/api/lowerHandParticipant.ts +++ b/src/frontend/src/features/rooms/livekit/api/lowerHandParticipant.ts @@ -2,6 +2,7 @@ import { Participant } from 'livekit-client' import { fetchServerApi } from './fetchServerApi' import { buildServerApiUrl } from './buildServerApiUrl' import { useRoomData } from '../hooks/useRoomData' +import { safeParseMetadata } from '@/features/rooms/utils/safeParseMetadata' export const useLowerHandParticipant = () => { const data = useRoomData() @@ -10,7 +11,7 @@ export const useLowerHandParticipant = () => { if (!data || !data?.livekit) { throw new Error('Room data is not available') } - const newMetadata = JSON.parse(participant.metadata || '{}') + const newMetadata = safeParseMetadata(participant.metadata) || {} newMetadata.raised = !newMetadata.raised return fetchServerApi( buildServerApiUrl( diff --git a/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantsList.tsx b/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantsList.tsx index 19b85a3a..d7759632 100644 --- a/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantsList.tsx +++ b/src/frontend/src/features/rooms/livekit/components/controls/Participants/ParticipantsList.tsx @@ -12,6 +12,7 @@ import { WaitingParticipantListItem } from './WaitingParticipantListItem' import { useWaitingParticipants } from '@/features/rooms/hooks/useWaitingParticipants' import { Participant } from 'livekit-client' import { WaitingParticipant } from '@/features/rooms/api/listWaitingParticipants' +import { safeParseMetadata } from '@/features/rooms/utils/safeParseMetadata' // TODO: Optimize rendering performance, especially for longer participant lists, even though they are generally short. export const ParticipantsList = () => { @@ -36,7 +37,7 @@ export const ParticipantsList = () => { ] const raisedHandParticipants = participants.filter((participant) => { - const data = JSON.parse(participant.metadata || '{}') + const data = safeParseMetadata(participant.metadata) return data.raised }) diff --git a/src/frontend/src/features/rooms/utils/safeParseMetadata.tsx b/src/frontend/src/features/rooms/utils/safeParseMetadata.tsx new file mode 100644 index 00000000..f7d74269 --- /dev/null +++ b/src/frontend/src/features/rooms/utils/safeParseMetadata.tsx @@ -0,0 +1,22 @@ +export const safeParseMetadata = ( + metadataStr: string | null | undefined +): Record => { + if (!metadataStr) { + return {} + } + + try { + const parsed = JSON.parse(metadataStr) + + // Ensure the result is an object + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { + console.warn('Metadata parsed to non-object value:', parsed) + return {} + } + + return parsed as Record + } catch (error) { + console.error('Failed to parse metadata:', error) + return {} + } +}