From bc3ddc04ca15c0626a60f3b241d67608c58abc45 Mon Sep 17 00:00:00 2001 From: Hristo Terezov Date: Mon, 12 Aug 2024 17:40:11 -0500 Subject: [PATCH] feat(shared-video): Allow only whitelisted URLs. --- lang/main.json | 2 +- .../components/ParticipantView.native.tsx | 4 +- react/features/shared-video/actions.any.ts | 4 ++ .../shared-video/components/index.native.ts | 1 + .../shared-video/components/index.web.ts | 1 + .../components/web/SharedVideo.tsx | 14 +++- react/features/shared-video/constants.ts | 10 +++ react/features/shared-video/functions.ts | 67 +++++++++++++++++-- react/features/shared-video/hooks.ts | 24 +++++++ react/features/shared-video/middleware.any.ts | 24 ++++++- react/features/shared-video/middleware.web.ts | 5 ++ .../components/native/OverflowMenu.tsx | 10 ++- react/features/toolbox/hooks.web.ts | 9 +-- 13 files changed, 158 insertions(+), 17 deletions(-) create mode 100644 react/features/shared-video/hooks.ts diff --git a/lang/main.json b/lang/main.json index a53c65a1f9ddf..74aca7c5c42a5 100644 --- a/lang/main.json +++ b/lang/main.json @@ -443,7 +443,7 @@ "shareVideoTitle": "Share video", "shareYourScreen": "Share your screen", "shareYourScreenDisabled": "Screen sharing disabled.", - "sharedVideoDialogError": "Error: Invalid URL", + "sharedVideoDialogError": "Error: Invalid or forbidden URL", "sharedVideoLinkPlaceholder": "YouTube link or direct video link", "show": "Show", "start": "Start ", diff --git a/react/features/base/participants/components/ParticipantView.native.tsx b/react/features/base/participants/components/ParticipantView.native.tsx index e8bd9cf6668d3..46ad03ad40a5d 100644 --- a/react/features/base/participants/components/ParticipantView.native.tsx +++ b/react/features/base/participants/components/ParticipantView.native.tsx @@ -8,6 +8,7 @@ import { isTrackStreamingStatusInactive } from '../../../connection-indicator/functions'; import SharedVideo from '../../../shared-video/components/native/SharedVideo'; +import { isSharedVideoEnabled } from '../../../shared-video/functions'; import { IStateful } from '../../app/types'; import Avatar from '../../avatar/components/Avatar'; import { translate } from '../../i18n/functions'; @@ -236,7 +237,8 @@ function _mapStateToProps(state: IReduxState, ownProps: any) { _isConnectionInactive: isTrackStreamingStatusInactive(videoTrack), _isSharedVideoParticipant: isSharedVideoParticipant(participant), _participantName: getParticipantDisplayName(state, participantId), - _renderVideo: shouldRenderParticipantVideo(state, participantId) && !disableVideo, + _renderVideo: shouldRenderParticipantVideo(state, participantId) && !disableVideo + && isSharedVideoEnabled(state), _videoTrack: videoTrack }; } diff --git a/react/features/shared-video/actions.any.ts b/react/features/shared-video/actions.any.ts index 5eba10bbbdd5d..26ed62ff2e34b 100644 --- a/react/features/shared-video/actions.any.ts +++ b/react/features/shared-video/actions.any.ts @@ -5,6 +5,7 @@ import { getLocalParticipant } from '../base/participants/functions'; import { RESET_SHARED_VIDEO_STATUS, SET_SHARED_VIDEO_STATUS } from './actionTypes'; import { SharedVideoDialog } from './components'; +import { isSharedVideoEnabled, isURLAllowedForSharedVideo } from './functions'; /** * Resets the status of the shared video. @@ -89,6 +90,9 @@ export function stopSharedVideo() { */ export function playSharedVideo(videoUrl: string) { return (dispatch: IStore['dispatch'], getState: IStore['getState']) => { + if (!isSharedVideoEnabled(getState()) || !isURLAllowedForSharedVideo(videoUrl)) { + return; + } const conference = getCurrentConference(getState()); if (conference) { diff --git a/react/features/shared-video/components/index.native.ts b/react/features/shared-video/components/index.native.ts index 9de9793a8bd7a..699685be5d6c1 100644 --- a/react/features/shared-video/components/index.native.ts +++ b/react/features/shared-video/components/index.native.ts @@ -1,2 +1,3 @@ // @ts-ignore export { default as SharedVideoDialog } from './native/SharedVideoDialog'; +export { default as SharedVideoButton } from './native/SharedVideoButton'; diff --git a/react/features/shared-video/components/index.web.ts b/react/features/shared-video/components/index.web.ts index 81c559db004d9..617f11369faa3 100644 --- a/react/features/shared-video/components/index.web.ts +++ b/react/features/shared-video/components/index.web.ts @@ -1 +1,2 @@ export { default as SharedVideoDialog } from './web/SharedVideoDialog'; +export { default as SharedVideoButton } from './web/SharedVideoButton'; diff --git a/react/features/shared-video/components/web/SharedVideo.tsx b/react/features/shared-video/components/web/SharedVideo.tsx index 2be6481b75677..600a192550848 100644 --- a/react/features/shared-video/components/web/SharedVideo.tsx +++ b/react/features/shared-video/components/web/SharedVideo.tsx @@ -7,6 +7,7 @@ import { IReduxState } from '../../../app/types'; import { getLocalParticipant } from '../../../base/participants/functions'; import { getVerticalViewMaxWidth } from '../../../filmstrip/functions.web'; import { getToolboxHeight } from '../../../toolbox/functions.web'; +import { isSharedVideoEnabled } from '../../functions'; import VideoManager from './VideoManager'; import YoutubeVideoManager from './YoutubeVideoManager'; @@ -33,6 +34,11 @@ interface IProps { */ filmstripWidth: number; + /** + * Whether the shared video is enabled or not. + */ + isEnabled: boolean; + /** * Is the video shared by the local user. */ @@ -118,7 +124,12 @@ class SharedVideo extends Component { * @returns {React$Element} */ render() { - const { isOwner, isResizing } = this.props; + const { isEnabled, isOwner, isResizing } = this.props; + + if (!isEnabled) { + return null; + } + const className = !isResizing && isOwner ? '' : 'disable-pointer'; return ( @@ -152,6 +163,7 @@ function _mapStateToProps(state: IReduxState) { clientWidth, filmstripVisible: visible, filmstripWidth: getVerticalViewMaxWidth(state), + isEnabled: isSharedVideoEnabled(state), isOwner: ownerId === localParticipant?.id, isResizing, videoUrl diff --git a/react/features/shared-video/constants.ts b/react/features/shared-video/constants.ts index 5250a917d79b3..af550dfc12e4d 100644 --- a/react/features/shared-video/constants.ts +++ b/react/features/shared-video/constants.ts @@ -28,3 +28,13 @@ export const PLAYBACK_STATUSES = { PAUSED: 'pause', STOPPED: 'stop' }; + +/** + * The domain for youtube URLs. + */ +export const YOUTUBE_URL_DOMAIN = 'youtube.com'; + +/** + * The white listed domains for shared video. + */ +export const URL_WHITELIST = [ YOUTUBE_URL_DOMAIN ]; diff --git a/react/features/shared-video/functions.ts b/react/features/shared-video/functions.ts index a41c1ffccb493..fd44d0e1e4a1c 100644 --- a/react/features/shared-video/functions.ts +++ b/react/features/shared-video/functions.ts @@ -1,7 +1,13 @@ import { IStateful } from '../base/app/types'; import { getFakeParticipants } from '../base/participants/functions'; +import { toState } from '../base/redux/functions'; -import { VIDEO_PLAYER_PARTICIPANT_NAME, YOUTUBE_PLAYER_PARTICIPANT_NAME } from './constants'; +import { + URL_WHITELIST, + VIDEO_PLAYER_PARTICIPANT_NAME, + YOUTUBE_PLAYER_PARTICIPANT_NAME, + YOUTUBE_URL_DOMAIN +} from './constants'; /** * Validates the entered video url. @@ -70,16 +76,22 @@ export function extractYoutubeIdOrURL(input: string) { return; } - const youtubeId = getYoutubeId(trimmedLink); + if (areYoutubeURLsAllowedForSharedVideo()) { + const youtubeId = getYoutubeId(trimmedLink); - if (youtubeId) { - return youtubeId; + if (youtubeId) { + return youtubeId; + } } // Check if the URL is valid, native may crash otherwise. try { // eslint-disable-next-line no-new - new URL(trimmedLink); + const url = new URL(trimmedLink); + + if (!url?.hostname || !URL_WHITELIST.includes(url?.hostname)) { + return; + } } catch (_) { return; } @@ -87,3 +99,48 @@ export function extractYoutubeIdOrURL(input: string) { return trimmedLink; } +/** + * Returns true if shared video functionality is enabled and false otherwise. + * + * @param {IStateful} stateful - - The redux store or {@code getState} function. + * @returns {boolean} + */ +export function isSharedVideoEnabled(stateful: IStateful) { + const state = toState(stateful); + const { disableThirdPartyRequests = false } = state['features/base/config']; + + return !disableThirdPartyRequests && URL_WHITELIST.length > 0; +} + +/** + * Checks if you youtube URLs should be allowed for shared videos. + * + * @returns {boolean} + */ +export function areYoutubeURLsAllowedForSharedVideo() { + return URL_WHITELIST.includes(YOUTUBE_URL_DOMAIN); +} + +/** + * Returns true if the passed url is allowed to be used for shared video or not. + * + * @param {string} url - The URL. + * @returns {boolean} + */ +export function isURLAllowedForSharedVideo(url: string) { + if (!url) { + return false; + } + + if (!url.match(/http/)) { + return areYoutubeURLsAllowedForSharedVideo(); + } + + try { + const urlObject = new URL(url); + + return URL_WHITELIST.includes(urlObject?.hostname); + } catch (_e) { + return false; + } +} diff --git a/react/features/shared-video/hooks.ts b/react/features/shared-video/hooks.ts new file mode 100644 index 0000000000000..7ba3bd6f39bef --- /dev/null +++ b/react/features/shared-video/hooks.ts @@ -0,0 +1,24 @@ +import { useSelector } from 'react-redux'; + +import { SharedVideoButton } from './components'; +import { isSharedVideoEnabled } from './functions'; + +const shareVideo = { + key: 'sharedvideo', + Content: SharedVideoButton, + group: 3 +}; + +/** + * A hook that returns the shared video button if it is enabled and undefined otherwise. + * + * @returns {Object | undefined} + */ +export function useSharedVideoButton() { + const sharedVideoEnabled = useSelector(isSharedVideoEnabled); + + if (sharedVideoEnabled) { + return shareVideo; + } +} + diff --git a/react/features/shared-video/middleware.any.ts b/react/features/shared-video/middleware.any.ts index d0f5deec88b7e..56f405cc78b6e 100644 --- a/react/features/shared-video/middleware.any.ts +++ b/react/features/shared-video/middleware.any.ts @@ -17,7 +17,7 @@ import { setSharedVideoStatus } from './actions.any'; import { PLAYBACK_STATUSES, SHARED_VIDEO, VIDEO_PLAYER_PARTICIPANT_NAME } from './constants'; -import { isSharingStatus } from './functions'; +import { isSharedVideoEnabled, isSharingStatus, isURLAllowedForSharedVideo } from './functions'; import logger from './logger'; @@ -34,6 +34,10 @@ MiddlewareRegistry.register(store => next => action => { switch (action.type) { case CONFERENCE_JOIN_IN_PROGRESS: { + if (!isSharedVideoEnabled(state)) { + break; + } + const { conference } = action; const localParticipantId = getLocalParticipant(state)?.id; @@ -41,6 +45,12 @@ MiddlewareRegistry.register(store => next => action => { ({ value, attributes }: { attributes: { from: string; muted: string; state: string; time: string; }; value: string; }) => { + if (!isURLAllowedForSharedVideo(value)) { + logger.debug(`Shared Video: Received a not allowed URL ${value}`); + + return; + } + const { from } = attributes; const sharedVideoStatus = attributes.state; @@ -62,9 +72,15 @@ MiddlewareRegistry.register(store => next => action => { break; } case CONFERENCE_LEFT: + if (!isSharedVideoEnabled(state)) { + break; + } dispatch(resetSharedVideoStatus()); break; case PARTICIPANT_LEFT: { + if (!isSharedVideoEnabled(state)) { + break; + } const conference = getCurrentConference(state); const { ownerId: stateOwnerId, videoUrl: statevideoUrl } = state['features/shared-video']; @@ -77,6 +93,9 @@ MiddlewareRegistry.register(store => next => action => { break; } case SET_SHARED_VIDEO_STATUS: { + if (!isSharedVideoEnabled(state)) { + break; + } const conference = getCurrentConference(state); const localParticipantId = getLocalParticipant(state)?.id; const { videoUrl, status, ownerId, time, muted, volume } = action; @@ -102,6 +121,9 @@ MiddlewareRegistry.register(store => next => action => { break; } case RESET_SHARED_VIDEO_STATUS: { + if (!isSharedVideoEnabled(state)) { + break; + } const localParticipantId = getLocalParticipant(state)?.id; const { ownerId: stateOwnerId, videoUrl: statevideoUrl } = state['features/shared-video']; diff --git a/react/features/shared-video/middleware.web.ts b/react/features/shared-video/middleware.web.ts index 24e647bfbc5af..f9f573f9e4d23 100644 --- a/react/features/shared-video/middleware.web.ts +++ b/react/features/shared-video/middleware.web.ts @@ -4,6 +4,7 @@ import MiddlewareRegistry from '../base/redux/MiddlewareRegistry'; import { setDisableButton } from './actions.web'; import { SHARED_VIDEO } from './constants'; +import { isSharedVideoEnabled } from './functions'; import './middleware.any'; @@ -13,6 +14,10 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => { switch (action.type) { case CONFERENCE_JOIN_IN_PROGRESS: { + if (!isSharedVideoEnabled(state)) { + break; + } + const { conference } = action; conference.addCommandListener(SHARED_VIDEO, ({ attributes }: { attributes: diff --git a/react/features/toolbox/components/native/OverflowMenu.tsx b/react/features/toolbox/components/native/OverflowMenu.tsx index 4186a434744a0..84cb421b99cc6 100644 --- a/react/features/toolbox/components/native/OverflowMenu.tsx +++ b/react/features/toolbox/components/native/OverflowMenu.tsx @@ -18,6 +18,7 @@ import RecordButton from '../../../recording/components/Recording/native/RecordB import SecurityDialogButton from '../../../security/components/security-dialog/native/SecurityDialogButton'; import SharedVideoButton from '../../../shared-video/components/native/SharedVideoButton'; +import { isSharedVideoEnabled } from '../../../shared-video/functions'; import SpeakerStatsButton from '../../../speaker-stats/components/native/SpeakerStatsButton'; import { isSpeakerStatsDisabled } from '../../../speaker-stats/functions'; import ClosedCaptionButton from '../../../subtitles/components/native/ClosedCaptionButton'; @@ -55,6 +56,11 @@ interface IProps { */ _isOpen: boolean; + /** + * Whether the shared video is enabled or not. + */ + _isSharedVideoEnabled: boolean; + /** * Whether or not speaker stats is disable. */ @@ -121,6 +127,7 @@ class OverflowMenu extends PureComponent { const { _isBreakoutRoomsSupported, _isSpeakerStatsDisabled, + _isSharedVideoEnabled, _shouldDisplayReactionsButtons, _width, dispatch @@ -168,7 +175,7 @@ class OverflowMenu extends PureComponent { {/* @ts-ignore */} - + {_isSharedVideoEnabled && } {!toolbarButtons.has('screensharing') && } {!_isSpeakerStatsDisabled && } {!toolbarButtons.has('tileview') && } @@ -255,6 +262,7 @@ function _mapStateToProps(state: IReduxState) { return { _customToolbarButtons: customToolbarButtons, _isBreakoutRoomsSupported: conference?.getBreakoutRooms()?.isSupported(), + _isSharedVideoEnabled: isSharedVideoEnabled(state), _isSpeakerStatsDisabled: isSpeakerStatsDisabled(state), _shouldDisplayReactionsButtons: shouldDisplayReactionsButtons(state), _width: state['features/base/responsive-ui'].clientWidth diff --git a/react/features/toolbox/hooks.web.ts b/react/features/toolbox/hooks.web.ts index 0607218be17ee..a6ce469f77ecf 100644 --- a/react/features/toolbox/hooks.web.ts +++ b/react/features/toolbox/hooks.web.ts @@ -45,7 +45,7 @@ import ShareAudioButton from '../screen-share/components/web/ShareAudioButton'; import { isScreenAudioSupported, isScreenVideoShared } from '../screen-share/functions'; import { useSecurityDialogButton } from '../security/hooks.web'; import SettingsButton from '../settings/components/web/SettingsButton'; -import SharedVideoButton from '../shared-video/components/web/SharedVideoButton'; +import { useSharedVideoButton } from '../shared-video/hooks'; import SpeakerStats from '../speaker-stats/components/web/SpeakerStats'; import { isSpeakerStatsDisabled } from '../speaker-stats/functions'; import { useSpeakerStatsButton } from '../speaker-stats/hooks.web'; @@ -142,12 +142,6 @@ const linkToSalesforce = { group: 2 }; -const shareVideo = { - key: 'sharedvideo', - Content: SharedVideoButton, - group: 3 -}; - const shareAudio = { key: 'shareaudio', Content: ShareAudioButton, @@ -288,6 +282,7 @@ export function useToolboxButtons( const liveStreaming = useLiveStreamingButton(); const linktosalesforce = useLinkToSalesforceButton(); const shareaudio = getShareAudioButton(); + const shareVideo = useSharedVideoButton(); const whiteboard = useWhiteboardButton(); const etherpad = useEtherpadButton(); const virtualBackground = useVirtualBackgroundButton();