fix: sanitize username input to prevent hyperlinks (#24210)

* fix(username): sanitize input to prevent hyperlinks

* fix(username): sanitize input to prevent hyperlinks

* Implemented CodeRabbits comments

* clean username before saving and keep input consistent

* sanitize initial username on load to avoid mismatch with input

* remove comments

* remove comments

* Used existing slugify lib instead of creating new one

---------

Co-authored-by: Dhairyashil <dhairyashilshinde@xoxo-2.local>
Co-authored-by: Devanshu Sharma <devanshusharma658@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
This commit is contained in:
Dhairyashil Shinde
2025-11-03 13:16:51 +05:30
committed by GitHub
co-authored by Dhairyashil Devanshu Sharma Anik Dhabal Babu Udit Takkar
parent 0ae6e92558
commit 7d16da5c98
3 changed files with 24 additions and 11 deletions
@@ -13,6 +13,7 @@ import { fetchUsername } from "@calcom/lib/fetchUsername";
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
import { useDebounce } from "@calcom/lib/hooks/useDebounce";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import slugify from "@calcom/lib/slugify";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import type { AppRouter } from "@calcom/trpc/types/server/routers/_app";
@@ -81,7 +82,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
useEffect(() => {
// Use the current username or if it's not set, use the one available from stripe
setInputUsernameValue(currentUsername || stripeCustomer?.username || "");
setInputUsernameValue(slugify(currentUsername || stripeCustomer?.username || "", true));
}, [setInputUsernameValue, currentUsername, stripeCustomer?.username]);
useEffect(() => {
@@ -105,7 +106,8 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
const updateUsername = trpc.viewer.me.updateProfile.useMutation({
onSuccess: async () => {
onSuccessMutation && (await onSuccessMutation());
await update({ username: inputUsernameValue });
const sanitizedUsername = slugify(inputUsernameValue || "");
await update({ username: sanitizedUsername });
setOpenDialogSaveUsername(false);
},
onError: (error) => {
@@ -171,11 +173,12 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
};
const saveUsername = () => {
const sanitizedUsername = slugify(inputUsernameValue || "");
if (usernameChangeCondition !== UsernameChangeStatusEnum.UPGRADE) {
updateUsername.mutate({
username: inputUsernameValue,
username: sanitizedUsername,
});
setCurrentUsername(inputUsernameValue);
setCurrentUsername(sanitizedUsername);
}
};
@@ -234,7 +237,8 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
if (searchParams?.toString() !== _searchParams.toString()) {
router.replace(`${pathname}?${_searchParams.toString()}`);
}
setInputUsernameValue(event.target.value);
const sanitized = slugify(event.target.value, true);
setInputUsernameValue(sanitized);
}}
data-testid="username-input"
/>
@@ -16,6 +16,7 @@ import { DialogContent, DialogFooter, DialogClose } from "@calcom/ui/components/
import { TextField } from "@calcom/ui/components/form";
import { Icon } from "@calcom/ui/components/icon";
import { Tooltip } from "@calcom/ui/components/tooltip";
import slugify from "@calcom/lib/slugify";
import type { TRPCClientErrorLike } from "@trpc/client";
@@ -72,10 +73,11 @@ const UsernameTextfield = (props: ICustomUsernameProps & Partial<React.Component
const updateUsernameMutation = trpc.viewer.me.updateProfile.useMutation({
onSuccess: async () => {
const sanitizedUsername = slugify(inputUsernameValue || "");
onSuccessMutation && (await onSuccessMutation());
setOpenDialogSaveUsername(false);
setCurrentUsername(inputUsernameValue);
await update({ username: inputUsernameValue });
setCurrentUsername(sanitizedUsername);
await update({ username: sanitizedUsername });
},
onError: (error) => {
onErrorMutation && onErrorMutation(error);
@@ -108,8 +110,9 @@ const UsernameTextfield = (props: ICustomUsernameProps & Partial<React.Component
};
const updateUsername = async () => {
const sanitizedUsername = slugify(inputUsernameValue || "");
updateUsernameMutation.mutate({
username: inputUsernameValue,
username: sanitizedUsername,
});
};
@@ -134,7 +137,8 @@ const UsernameTextfield = (props: ICustomUsernameProps & Partial<React.Component
)}
onChange={(event) => {
event.preventDefault();
setInputUsernameValue(event.target.value);
const sanitized = slugify(event.target.value, true);
setInputUsernameValue(sanitized);
}}
data-testid="username-input"
{...rest}
@@ -6,6 +6,7 @@ import { Controller, useForm } from "react-hook-form";
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
import { WEBSITE_URL, IS_SELF_HOSTED } from "@calcom/lib/constants";
import slugify from "@calcom/lib/slugify";
import { trpc } from "@calcom/trpc/react";
import type { AppRouter } from "@calcom/trpc/types/server/routers/_app";
@@ -56,7 +57,7 @@ export const UsernameAvailabilityField = ({
: { username: currentUsernameState || "", setQuery: setCurrentUsernameState };
const formMethods = useForm({
defaultValues: {
username: currentUsername,
username: slugify(currentUsername || user.username || ""),
},
});
@@ -78,7 +79,11 @@ export const UsernameAvailabilityField = ({
setCurrentUsername={setCurrentUsername}
inputUsernameValue={value}
usernameRef={ref}
setInputUsernameValue={onChange}
setInputUsernameValue={(val) => {
const displayValue = slugify(val, true);
formMethods.setValue("username", displayValue);
onChange?.(displayValue);
}}
onSuccessMutation={onSuccessMutation}
onErrorMutation={onErrorMutation}
disabled={!!user.organization?.id}