From 7ffb213ce7e2c30bc0a1fa1ca7ae396966d6f0aa Mon Sep 17 00:00:00 2001 From: DuckieTM Date: Mon, 23 Mar 2026 22:14:03 +0100 Subject: [PATCH] =?UTF-8?q?=E3=8A=99=EF=B8=8F=20Security=20Fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - XSS fix: Created SanitizeHtml.ts utility using DOMPurify (already in package.json but never used). Wrapped all 21 dangerouslySetInnerHTML calls in catalog views with SanitizeHtml() — only allows safe tags (b, i, u, br, span, div, p, a, strong, em, img) - Race condition fix: Added 10-second timeout fallbacks on purchase flags in CatalogPurchaseWidgetView and CatalogGiftView so the flag auto-resets even if the server never responds --- src/api/utils/SanitizeHtml.ts | 10 ++++++++++ src/api/utils/index.ts | 1 + src/components/catalog/CatalogModernView.tsx | 2 +- src/components/catalog/views/gift/CatalogGiftView.tsx | 1 + .../page/layout/CatalogLayoutBadgeDisplayView.tsx | 4 ++-- .../page/layout/CatalogLayoutColorGroupingView.tsx | 4 ++-- .../page/layout/CatalogLayoutCustomPrefixView.tsx | 4 ++-- .../views/page/layout/CatalogLayoutDefaultView.tsx | 4 ++-- .../page/layout/CatalogLayoutGuildCustomFurniView.tsx | 3 ++- .../views/page/layout/CatalogLayoutGuildForumView.tsx | 4 ++-- .../page/layout/CatalogLayoutGuildFrontpageView.tsx | 8 ++++---- .../views/page/layout/CatalogLayoutInfoLoyaltyView.tsx | 3 ++- .../views/page/layout/CatalogLayoutPets3View.tsx | 7 ++++--- .../views/page/layout/CatalogLayoutRoomBundleView.tsx | 3 ++- .../page/layout/CatalogLayoutSingleBundleView.tsx | 3 ++- .../page/layout/CatalogLayoutSoundMachineView.tsx | 4 ++-- .../views/page/layout/CatalogLayoutSpacesView.tsx | 3 ++- .../views/page/layout/CatalogLayoutTrophiesView.tsx | 4 ++-- .../views/page/layout/CatalogLayoutVipBuyView.tsx | 6 +++--- .../views/page/layout/pets/CatalogLayoutPetView.tsx | 4 ++-- .../views/page/widgets/CatalogPurchaseWidgetView.tsx | 2 ++ .../catalog/views/targeted-offer/OfferWindowView.tsx | 4 ++-- 22 files changed, 54 insertions(+), 34 deletions(-) create mode 100644 src/api/utils/SanitizeHtml.ts diff --git a/src/api/utils/SanitizeHtml.ts b/src/api/utils/SanitizeHtml.ts new file mode 100644 index 0000000..39af6e9 --- /dev/null +++ b/src/api/utils/SanitizeHtml.ts @@ -0,0 +1,10 @@ +import DOMPurify from 'dompurify'; + +export const SanitizeHtml = (html: string): string => +{ + return DOMPurify.sanitize(html, { + ALLOWED_TAGS: [ 'b', 'i', 'u', 'br', 'span', 'div', 'p', 'a', 'strong', 'em', 'img' ], + ALLOWED_ATTR: [ 'href', 'target', 'class', 'style', 'src', 'alt' ], + ALLOW_DATA_ATTR: false + }); +}; diff --git a/src/api/utils/index.ts b/src/api/utils/index.ts index 4a4b221..1f22e7f 100644 --- a/src/api/utils/index.ts +++ b/src/api/utils/index.ts @@ -15,6 +15,7 @@ export * from './PrefixUtils'; export * from './ProductImageUtility'; export * from './Randomizer'; export * from './RoomChatFormatter'; +export * from './SanitizeHtml'; export * from './SetLocalStorage'; export * from './SoundNames'; export * from './WindowSaveOptions'; diff --git a/src/components/catalog/CatalogModernView.tsx b/src/components/catalog/CatalogModernView.tsx index 958e3ea..ec19b70 100644 --- a/src/components/catalog/CatalogModernView.tsx +++ b/src/components/catalog/CatalogModernView.tsx @@ -228,7 +228,7 @@ const CatalogModernViewInner: FC<{}> = () => { activeNodes && activeNodes.length > 0 ? activeNodes.map((node, i) => ( - + { i > 0 && } activateNode(node) : undefined }> diff --git a/src/components/catalog/views/gift/CatalogGiftView.tsx b/src/components/catalog/views/gift/CatalogGiftView.tsx index 8104afb..e23e568 100644 --- a/src/components/catalog/views/gift/CatalogGiftView.tsx +++ b/src/components/catalog/views/gift/CatalogGiftView.tsx @@ -128,6 +128,7 @@ export const CatalogGiftView: FC<{}> = props => if(isBuyingGift) return; isBuyingGift = true; + setTimeout(() => { isBuyingGift = false; }, 10000); SendMessageComposer(new PurchaseFromCatalogAsGiftComposer(pageId, offerId, extraData, receiverName, message, colourId, selectedBoxIndex, selectedRibbonIndex, showMyFace)); return; diff --git a/src/components/catalog/views/page/layout/CatalogLayoutBadgeDisplayView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutBadgeDisplayView.tsx index f2bd6f3..ee82e6e 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutBadgeDisplayView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutBadgeDisplayView.tsx @@ -1,5 +1,5 @@ import { FC } from 'react'; -import { LocalizeText } from '../../../../../api'; +import { LocalizeText, SanitizeHtml } from '../../../../../api'; import { Column, Grid, Text } from '../../../../../common'; import { useCatalog } from '../../../../../hooks'; import { CatalogBadgeSelectorWidgetView } from '../widgets/CatalogBadgeSelectorWidgetView'; @@ -31,7 +31,7 @@ export const CatalogLayoutBadgeDisplayView: FC = props => { !currentOffer && <> { !!page.localization.getImage(1) && } - + } { currentOffer && <> diff --git a/src/components/catalog/views/page/layout/CatalogLayoutColorGroupingView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutColorGroupingView.tsx index 47e0dc6..4c1944c 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutColorGroupingView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutColorGroupingView.tsx @@ -1,7 +1,7 @@ import { ColorConverter } from '@nitrots/nitro-renderer'; import { FC, useMemo, useState } from 'react'; import { FaFillDrip } from 'react-icons/fa'; -import { IPurchasableOffer } from '../../../../../api'; +import { IPurchasableOffer, SanitizeHtml } from '../../../../../api'; import { AutoGrid, Button, Column, Grid, LayoutGridItem, Text } from '../../../../../common'; import { useCatalog } from '../../../../../hooks'; import { CatalogGridOfferView } from '../common/CatalogGridOfferView'; @@ -146,7 +146,7 @@ export const CatalogLayoutColorGroupingView: FC { !!page.localization.getImage(1) && } - + } { currentOffer && <> diff --git a/src/components/catalog/views/page/layout/CatalogLayoutCustomPrefixView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutCustomPrefixView.tsx index 65f9a21..e723833 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutCustomPrefixView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutCustomPrefixView.tsx @@ -1,6 +1,6 @@ import { PurchasePrefixComposer } from '@nitrots/nitro-renderer'; import { FC, useEffect, useMemo, useRef, useState } from 'react'; -import { LocalizeText, SendMessageComposer, PRESET_PREFIX_EFFECTS, parsePrefixColors, getPrefixEffectStyle, PREFIX_EFFECT_KEYFRAMES } from '../../../../../api'; +import { LocalizeText, SanitizeHtml, SendMessageComposer, PRESET_PREFIX_EFFECTS, parsePrefixColors, getPrefixEffectStyle, PREFIX_EFFECT_KEYFRAMES } from '../../../../../api'; import { CatalogLayoutProps } from './CatalogLayout.types'; import data from '@emoji-mart/data'; import Picker from '@emoji-mart/react'; @@ -137,7 +137,7 @@ export const CatalogLayoutCustomPrefixView: FC = props => { page.localization.getImage(0) && } { page.localization.getText(0) && -
} +
} { /* Live Preview */ }
= props =>
{ !!page.localization.getImage(1) && } - +
} { /* Item grid */ } diff --git a/src/components/catalog/views/page/layout/CatalogLayoutGuildCustomFurniView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutGuildCustomFurniView.tsx index f94bd0e..cb8b6fa 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutGuildCustomFurniView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutGuildCustomFurniView.tsx @@ -1,4 +1,5 @@ import { FC } from 'react'; +import { SanitizeHtml } from '../../../../../api'; import { Column, Grid, Text } from '../../../../../common'; import { useCatalog } from '../../../../../hooks'; import { CatalogGuildBadgeWidgetView } from '../widgets/CatalogGuildBadgeWidgetView'; @@ -23,7 +24,7 @@ export const CatalogLayouGuildCustomFurniView: FC = props => { !currentOffer && <> { !!page.localization.getImage(1) && } - + } { currentOffer && <> diff --git a/src/components/catalog/views/page/layout/CatalogLayoutGuildForumView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutGuildForumView.tsx index c21b204..21aabf4 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutGuildForumView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutGuildForumView.tsx @@ -1,6 +1,6 @@ import { CatalogGroupsComposer } from '@nitrots/nitro-renderer'; import { FC, useEffect, useState } from 'react'; -import { SendMessageComposer } from '../../../../../api'; +import { SanitizeHtml, SendMessageComposer } from '../../../../../api'; import { Column, Grid, Text } from '../../../../../common'; import { useCatalog } from '../../../../../hooks'; import { CatalogFirstProductSelectorWidgetView } from '../widgets/CatalogFirstProductSelectorWidgetView'; @@ -26,7 +26,7 @@ export const CatalogLayouGuildForumView: FC = props => -
+
{ !!currentOffer && diff --git a/src/components/catalog/views/page/layout/CatalogLayoutGuildFrontpageView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutGuildFrontpageView.tsx index 44f66b8..7a91686 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutGuildFrontpageView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutGuildFrontpageView.tsx @@ -1,6 +1,6 @@ import { CreateLinkEvent } from '@nitrots/nitro-renderer'; import { FC } from 'react'; -import { LocalizeText } from '../../../../../api'; +import { LocalizeText, SanitizeHtml } from '../../../../../api'; import { Button } from '../../../../../common/Button'; import { Column } from '../../../../../common/Column'; import { Grid } from '../../../../../common/Grid'; @@ -14,9 +14,9 @@ export const CatalogLayouGuildFrontpageView: FC = props => return ( -
-
-
+
+
+
diff --git a/src/components/catalog/views/page/layout/CatalogLayoutInfoLoyaltyView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutInfoLoyaltyView.tsx index a2a6a62..f5b1e4d 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutInfoLoyaltyView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutInfoLoyaltyView.tsx @@ -1,4 +1,5 @@ import { FC } from 'react'; +import { SanitizeHtml } from '../../../../../api'; import { CatalogLayoutProps } from './CatalogLayout.types'; export const CatalogLayoutInfoLoyaltyView: FC = props => @@ -8,7 +9,7 @@ export const CatalogLayoutInfoLoyaltyView: FC = props => return (
-
+
); diff --git a/src/components/catalog/views/page/layout/CatalogLayoutPets3View.tsx b/src/components/catalog/views/page/layout/CatalogLayoutPets3View.tsx index caba81a..d6dfe79 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutPets3View.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutPets3View.tsx @@ -1,5 +1,6 @@ import { FC } from 'react'; import { FaPaw } from 'react-icons/fa'; +import { SanitizeHtml } from '../../../../../api'; import { CatalogLayoutProps } from './CatalogLayout.types'; export const CatalogLayoutPets3View: FC = props => @@ -16,20 +17,20 @@ export const CatalogLayoutPets3View: FC = props =>
- +
{ /* Content */ }
-
+
{ /* Footer */ } { !!page.localization.getText(3) &&
- +
}
); diff --git a/src/components/catalog/views/page/layout/CatalogLayoutRoomBundleView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutRoomBundleView.tsx index 54df095..44358ff 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutRoomBundleView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutRoomBundleView.tsx @@ -1,4 +1,5 @@ import { FC } from 'react'; +import { SanitizeHtml } from '../../../../../api'; import { Column, Grid, Text } from '../../../../../common'; import { CatalogAddOnBadgeWidgetView } from '../widgets/CatalogAddOnBadgeWidgetView'; import { CatalogBundleGridWidgetView } from '../widgets/CatalogBundleGridWidgetView'; @@ -17,7 +18,7 @@ export const CatalogLayoutRoomBundleView: FC = props => { !!page.localization.getText(2) && - } + } diff --git a/src/components/catalog/views/page/layout/CatalogLayoutSingleBundleView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutSingleBundleView.tsx index 99357a8..7b8f1ec 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutSingleBundleView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutSingleBundleView.tsx @@ -1,4 +1,5 @@ import { FC } from 'react'; +import { SanitizeHtml } from '../../../../../api'; import { Column, Grid, Text } from '../../../../../common'; import { CatalogAddOnBadgeWidgetView } from '../widgets/CatalogAddOnBadgeWidgetView'; import { CatalogBundleGridWidgetView } from '../widgets/CatalogBundleGridWidgetView'; @@ -17,7 +18,7 @@ export const CatalogLayoutSingleBundleView: FC = props => { !!page.localization.getText(2) && - } + } diff --git a/src/components/catalog/views/page/layout/CatalogLayoutSoundMachineView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutSoundMachineView.tsx index 0df267d..0aa131e 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutSoundMachineView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutSoundMachineView.tsx @@ -1,6 +1,6 @@ import { GetOfficialSongIdMessageComposer, GetSoundManager, MusicPriorities, OfficialSongIdMessageEvent } from '@nitrots/nitro-renderer'; import { FC, useEffect, useState } from 'react'; -import { GetConfigurationValue, LocalizeText, ProductTypeEnum, SendMessageComposer } from '../../../../../api'; +import { GetConfigurationValue, LocalizeText, ProductTypeEnum, SanitizeHtml, SendMessageComposer } from '../../../../../api'; import { Button, Column, Grid, LayoutImage, Text } from '../../../../../common'; import { useCatalog, useMessageEvent } from '../../../../../hooks'; import { CatalogHeaderView } from '../../catalog-header/CatalogHeaderView'; @@ -80,7 +80,7 @@ export const CatalogLayoutSoundMachineView: FC = props => <> { !!page.localization.getImage(1) && } - + } { currentOffer && <> diff --git a/src/components/catalog/views/page/layout/CatalogLayoutSpacesView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutSpacesView.tsx index 32865ce..9cd18b6 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutSpacesView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutSpacesView.tsx @@ -1,4 +1,5 @@ import { FC, useEffect } from 'react'; +import { SanitizeHtml } from '../../../../../api'; import { Column, Grid, Text } from '../../../../../common'; import { useCatalog } from '../../../../../hooks'; import { CatalogPurchaseWidgetView } from '../widgets/CatalogPurchaseWidgetView'; @@ -26,7 +27,7 @@ export const CatalogLayoutSpacesView: FC = props => { !currentOffer && <> { !!page.localization.getImage(1) && } - + } { currentOffer && <> diff --git a/src/components/catalog/views/page/layout/CatalogLayoutTrophiesView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutTrophiesView.tsx index d192b81..e576695 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutTrophiesView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutTrophiesView.tsx @@ -1,6 +1,6 @@ import { FC, useEffect, useState } from 'react'; import { FaEdit, FaPen, FaPlus, FaTrophy } from 'react-icons/fa'; -import { LocalizeText, ProductTypeEnum } from '../../../../../api'; +import { LocalizeText, ProductTypeEnum, SanitizeHtml } from '../../../../../api'; import { Text } from '../../../../../common'; import { useCatalog } from '../../../../../hooks'; import { useCatalogAdmin } from '../../../CatalogAdminContext'; @@ -99,7 +99,7 @@ export const CatalogLayoutTrophiesView: FC = props => { LocalizeText('catalog.trophies.title') }
- +
} diff --git a/src/components/catalog/views/page/layout/CatalogLayoutVipBuyView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutVipBuyView.tsx index 1cb5283..2a9d40f 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutVipBuyView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutVipBuyView.tsx @@ -1,6 +1,6 @@ import { ClubOfferData, GetClubOffersMessageComposer, PurchaseFromCatalogComposer } from '@nitrots/nitro-renderer'; import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { CatalogPurchaseState, LocalizeText, SendMessageComposer } from '../../../../../api'; +import { CatalogPurchaseState, LocalizeText, SanitizeHtml, SendMessageComposer } from '../../../../../api'; import { AutoGrid, Button, Column, Flex, Grid, LayoutCurrencyIcon, LayoutGridItem, LayoutLoadingSpinnerView, Text } from '../../../../../common'; import { CatalogEvent, CatalogPurchaseFailureEvent, CatalogPurchasedEvent } from '../../../../../events'; import { useCatalog, usePurse, useUiEvent } from '../../../../../hooks'; @@ -160,12 +160,12 @@ export const CatalogLayoutVipBuyView: FC = props => ); }) } - + { currentPage.localization.getImage(1) && } - + { pendingOffer && diff --git a/src/components/catalog/views/page/layout/pets/CatalogLayoutPetView.tsx b/src/components/catalog/views/page/layout/pets/CatalogLayoutPetView.tsx index 850d085..4897797 100644 --- a/src/components/catalog/views/page/layout/pets/CatalogLayoutPetView.tsx +++ b/src/components/catalog/views/page/layout/pets/CatalogLayoutPetView.tsx @@ -1,7 +1,7 @@ import { ApproveNameMessageComposer, ApproveNameMessageEvent, ColorConverter, GetSellablePetPalettesComposer, PurchaseFromCatalogComposer, SellablePetPaletteData } from '@nitrots/nitro-renderer'; import { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { FaCheck, FaEdit, FaFillDrip, FaPaw, FaPlus, FaTimes } from 'react-icons/fa'; -import { DispatchUiEvent, GetPetAvailableColors, GetPetIndexFromLocalization, LocalizeText, SendMessageComposer } from '../../../../../../api'; +import { DispatchUiEvent, GetPetAvailableColors, GetPetIndexFromLocalization, LocalizeText, SanitizeHtml, SendMessageComposer } from '../../../../../../api'; import { LayoutGridItem, LayoutPetImageView } from '../../../../../../common'; import { CatalogPurchaseFailureEvent } from '../../../../../../events'; import { useCatalog, useMessageEvent } from '../../../../../../hooks'; @@ -249,7 +249,7 @@ export const CatalogLayoutPetView: FC = props => Offer: { currentOffer.offerId }
} { !!page.localization.getText(0) && -

} +

}

{ /* Name input */ } diff --git a/src/components/catalog/views/page/widgets/CatalogPurchaseWidgetView.tsx b/src/components/catalog/views/page/widgets/CatalogPurchaseWidgetView.tsx index 7f2837d..744cac7 100644 --- a/src/components/catalog/views/page/widgets/CatalogPurchaseWidgetView.tsx +++ b/src/components/catalog/views/page/widgets/CatalogPurchaseWidgetView.tsx @@ -87,6 +87,8 @@ export const CatalogPurchaseWidgetView: FC = pro isPurchasingCatalogItem = true; setPurchaseState(CatalogPurchaseState.PURCHASE); + setTimeout(() => { isPurchasingCatalogItem = false; }, 10000); + if(purchaseCallback) { purchaseCallback(); diff --git a/src/components/catalog/views/targeted-offer/OfferWindowView.tsx b/src/components/catalog/views/targeted-offer/OfferWindowView.tsx index 0d00732..72f0658 100644 --- a/src/components/catalog/views/targeted-offer/OfferWindowView.tsx +++ b/src/components/catalog/views/targeted-offer/OfferWindowView.tsx @@ -1,6 +1,6 @@ import { GetTargetedOfferComposer, PurchaseTargetedOfferComposer, TargetedOfferData } from '@nitrots/nitro-renderer'; import { Dispatch, SetStateAction, useMemo, useState } from 'react'; -import { FriendlyTime, GetConfigurationValue, LocalizeText, SendMessageComposer } from '../../../../api'; +import { FriendlyTime, GetConfigurationValue, LocalizeText, SanitizeHtml, SendMessageComposer } from '../../../../api'; import { Button, Column, Flex, LayoutCurrencyIcon, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../../../common'; import { usePurse } from '../../../../hooks'; @@ -63,7 +63,7 @@ export const OfferWindowView = (props: { offer: TargetedOfferData, setOpen: Disp

{ LocalizeText(offer.title) }

-
+
{ offer.purchaseLimit > 1 &&