Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { IUser, AvatarObject } from '@rocket.chat/core-typings';
import { Box, Button, Avatar, TextInput, IconButton, Label } from '@rocket.chat/fuselage';
import { Box, Button, Avatar, TextInput, IconButton, Label, FieldError } from '@rocket.chat/fuselage';
import { UserAvatar } from '@rocket.chat/ui-avatar';
import { useToastMessageDispatch, useSetting } from '@rocket.chat/ui-contexts';
import type { ReactElement, ChangeEvent } from 'react';
Expand All @@ -21,6 +21,15 @@ type UserAvatarEditorProps = {
name: IUser['name'];
};

function isUrl(myUrlString: string) {
try {
new URL(myUrlString);
return true;
} catch {
return false;
}
}
Comment on lines +24 to +31
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add protocol validation to prevent potential security issues.

The URL validation accepts any valid URL protocol, including potentially unsafe ones like javascript: or file:. For avatar URLs, you should restrict to safe protocols only.

Apply this diff to add protocol validation:

 function isUrl(myUrlString: string) {
 	try {
-		new URL(myUrlString);
-		return true;
+		const url = new URL(myUrlString);
+		return ['https:', 'http:', 'data:'].includes(url.protocol);
 	} catch {
 		return false;
 	}
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function isUrl(myUrlString: string) {
try {
new URL(myUrlString);
return true;
} catch {
return false;
}
}
function isUrl(myUrlString: string) {
try {
const url = new URL(myUrlString);
return ['https:', 'http:', 'data:'].includes(url.protocol);
} catch {
return false;
}
}
🤖 Prompt for AI Agents
In apps/meteor/client/components/avatar/UserAvatarEditor/UserAvatarEditor.tsx
around lines 24-31, the isUrl function currently accepts any URL protocol;
update it to only allow safe protocols (at minimum 'http:' and 'https:') and
optionally allow 'data:' for inline images. Keep the try/catch URL parsing, then
check url.protocol against an allowed set (normalize to lowercase); if allowing
'data:' ensure the pathname or href indicates an image MIME type (e.g.,
data:image/) before returning true; otherwise return false.


function UserAvatarEditor({ currentUsername, username, setAvatarObj, name, disabled, etag }: UserAvatarEditorProps): ReactElement {
const { t } = useTranslation();
const useFullNameForDefaultAvatar = useSetting('UI_Use_Name_Avatar');
Expand All @@ -29,6 +38,7 @@ function UserAvatarEditor({ currentUsername, username, setAvatarObj, name, disab
const [newAvatarSource, setNewAvatarSource] = useState<string>();
const imageUrlField = useId();
const dispatchToastMessage = useToastMessageDispatch();
const [avatarUrlError, setAvatarUrlError] = useState<string | undefined>(undefined);

const setUploadedPreview = useCallback(
async (file: File, avatarObj: AvatarObject) => {
Expand All @@ -49,8 +59,12 @@ function UserAvatarEditor({ currentUsername, username, setAvatarObj, name, disab
const [clickUpload] = useSingleFileInput(setUploadedPreview);

const handleAddUrl = (): void => {
setNewAvatarSource(avatarFromUrl);
setAvatarObj({ avatarUrl: avatarFromUrl });
if (isUrl(avatarFromUrl)) {
setNewAvatarSource(avatarFromUrl);
setAvatarObj({ avatarUrl: avatarFromUrl });
} else {
setAvatarUrlError(t('error-invalid-image-url'));
}
};

const clickReset = (): void => {
Expand All @@ -61,7 +75,11 @@ function UserAvatarEditor({ currentUsername, username, setAvatarObj, name, disab
const url = newAvatarSource;

const handleAvatarFromUrlChange = (event: ChangeEvent<HTMLInputElement>): void => {
setAvatarFromUrl(event.currentTarget.value);
if (avatarUrlError) {
setAvatarUrlError(undefined);
}
const { value } = event.currentTarget;
setAvatarFromUrl(value);
};

const handleSelectSuggestion = useCallback(
Expand All @@ -87,7 +105,7 @@ function UserAvatarEditor({ currentUsername, username, setAvatarObj, name, disab
imageOrientation: rotateImages ? 'from-image' : 'none',
objectFit: 'contain',
}}
onError={() => dispatchToastMessage({ type: 'error', message: t('error-invalid-image-url') })}
onError={() => setAvatarUrlError(t('error-invalid-image-url'))}
/>
<Box display='flex' flexDirection='column' flexGrow='1' justifyContent='space-between' mis={4}>
<Box display='flex' flexDirection='row' mbs='none'>
Expand All @@ -98,7 +116,7 @@ function UserAvatarEditor({ currentUsername, username, setAvatarObj, name, disab
<IconButton
icon='permalink'
secondary
disabled={disabled || !avatarFromUrl}
disabled={disabled || !avatarFromUrl || !!avatarUrlError}
title={t('Add_URL')}
mi={4}
onClick={handleAddUrl}
Expand All @@ -116,6 +134,11 @@ function UserAvatarEditor({ currentUsername, username, setAvatarObj, name, disab
mis={4}
onChange={handleAvatarFromUrlChange}
/>
{avatarUrlError && (
<FieldError aria-live='assertive' id={`${imageUrlField}-error`}>
{avatarUrlError}
</FieldError>
)}
</Box>
</Box>
</Box>
Expand Down
Loading