🔒️(frontend) implement strict validation for user-provided metadata

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.
This commit is contained in:
lebaudantoine
2025-03-04 00:12:43 +01:00
committed by aleb_the_flash
parent b73f18419b
commit 6545ecf11a
4 changed files with 30 additions and 5 deletions

View File

@@ -18,6 +18,7 @@ import {
ANIMATION_DURATION, ANIMATION_DURATION,
ReactionPortals, ReactionPortals,
} from '@/features/rooms/livekit/components/ReactionPortal' } from '@/features/rooms/livekit/components/ReactionPortal'
import { safeParseMetadata } from '@/features/rooms/utils/safeParseMetadata'
export const MainNotificationToast = () => { export const MainNotificationToast = () => {
const room = useRoomContext() const room = useRoomContext()
@@ -145,10 +146,10 @@ export const MainNotificationToast = () => {
if (isMobileBrowser()) return if (isMobileBrowser()) return
if (participant.isLocal) return if (participant.isLocal) return
const prevMetadata = JSON.parse(prevMetadataStr || '{}') const prevMetadata = safeParseMetadata(prevMetadataStr)
const metadata = JSON.parse(participant.metadata || '{}') const metadata = safeParseMetadata(participant.metadata)
if (prevMetadata.raised == metadata.raised) return if (prevMetadata?.raised == metadata?.raised) return
const existingToast = toastQueue.visibleToasts.find( const existingToast = toastQueue.visibleToasts.find(
(toast) => (toast) =>

View File

@@ -2,6 +2,7 @@ import { Participant } from 'livekit-client'
import { fetchServerApi } from './fetchServerApi' import { fetchServerApi } from './fetchServerApi'
import { buildServerApiUrl } from './buildServerApiUrl' import { buildServerApiUrl } from './buildServerApiUrl'
import { useRoomData } from '../hooks/useRoomData' import { useRoomData } from '../hooks/useRoomData'
import { safeParseMetadata } from '@/features/rooms/utils/safeParseMetadata'
export const useLowerHandParticipant = () => { export const useLowerHandParticipant = () => {
const data = useRoomData() const data = useRoomData()
@@ -10,7 +11,7 @@ export const useLowerHandParticipant = () => {
if (!data || !data?.livekit) { if (!data || !data?.livekit) {
throw new Error('Room data is not available') throw new Error('Room data is not available')
} }
const newMetadata = JSON.parse(participant.metadata || '{}') const newMetadata = safeParseMetadata(participant.metadata) || {}
newMetadata.raised = !newMetadata.raised newMetadata.raised = !newMetadata.raised
return fetchServerApi( return fetchServerApi(
buildServerApiUrl( buildServerApiUrl(

View File

@@ -12,6 +12,7 @@ import { WaitingParticipantListItem } from './WaitingParticipantListItem'
import { useWaitingParticipants } from '@/features/rooms/hooks/useWaitingParticipants' import { useWaitingParticipants } from '@/features/rooms/hooks/useWaitingParticipants'
import { Participant } from 'livekit-client' import { Participant } from 'livekit-client'
import { WaitingParticipant } from '@/features/rooms/api/listWaitingParticipants' 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. // TODO: Optimize rendering performance, especially for longer participant lists, even though they are generally short.
export const ParticipantsList = () => { export const ParticipantsList = () => {
@@ -36,7 +37,7 @@ export const ParticipantsList = () => {
] ]
const raisedHandParticipants = participants.filter((participant) => { const raisedHandParticipants = participants.filter((participant) => {
const data = JSON.parse(participant.metadata || '{}') const data = safeParseMetadata(participant.metadata)
return data.raised return data.raised
}) })

View File

@@ -0,0 +1,22 @@
export const safeParseMetadata = (
metadataStr: string | null | undefined
): Record<string, unknown> => {
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<string, unknown>
} catch (error) {
console.error('Failed to parse metadata:', error)
return {}
}
}