From f74419ff21f1930c77a410aca8149cf27916c684 Mon Sep 17 00:00:00 2001 From: Gandalf Date: Tue, 23 Dec 2025 02:01:06 +0100 Subject: [PATCH] fix(security): Prevent CSS injection in profile cover images Add validation and escaping for cover image URLs used in CSS contexts. Changes: - Create css-utils with escapeCssUrl() and isSafeImageUrl() - Migrate wallet profile-layout from proxifyImageUrl to proxifyImageSrc - Add CSS escaping to both blog and wallet profile layouts - Export css-utils from @ui/components The wallet was using the older proxifyImageUrl which doesn't encode URLs, making it vulnerable to CSS injection. Now both apps use proxifyImageSrc (Base58 encoding) plus CSS escaping for defense-in-depth. --- .../layouts/user-profile/profile-layout.tsx | 27 +++++++++------ .../components/common/profile-layout.tsx | 30 +++++++++++------ packages/ui/components/index.tsx | 1 + packages/ui/lib/css-utils.ts | 33 +++++++++++++++++++ 4 files changed, 71 insertions(+), 20 deletions(-) create mode 100644 packages/ui/lib/css-utils.ts diff --git a/apps/blog/features/layouts/user-profile/profile-layout.tsx b/apps/blog/features/layouts/user-profile/profile-layout.tsx index 6fb3fc566..c06377285 100644 --- a/apps/blog/features/layouts/user-profile/profile-layout.tsx +++ b/apps/blog/features/layouts/user-profile/profile-layout.tsx @@ -8,7 +8,7 @@ import { useQuery } from '@tanstack/react-query'; import env from '@beam-australia/react-env'; import { useTranslation } from '@/blog/i18n/client'; -import { Avatar, AvatarFallback, AvatarImage, proxifyImageSrc, getUserAvatarUrl } from '@ui/components'; +import { Avatar, AvatarFallback, AvatarImage, proxifyImageSrc, getUserAvatarUrl, escapeCssUrl, isSafeImageUrl } from '@ui/components'; import { Separator } from '@hive/ui/components/separator'; import TimeAgo from '@ui/components/time-ago'; import { Icons } from '@hive/ui/components/icons'; @@ -29,6 +29,21 @@ import { getTwitterInfo } from '@transaction/lib/custom-api'; import ListItem from './list-item'; import { useUserClient } from '@smart-signer/lib/auth/use-user-client'; +const getCoverImageStyle = (profileData: { posting_json_metadata?: string } | null): string => { + try { + if (!profileData?.posting_json_metadata) return ''; + const metadata = JSON.parse(profileData.posting_json_metadata); + const coverImage = metadata?.profile?.cover_image; + + if (!coverImage || !isSafeImageUrl(coverImage)) return ''; + + const proxifiedUrl = proxifyImageSrc(coverImage, 2048, 512); + return `url('${escapeCssUrl(proxifiedUrl.replace(/ /g, '%20'))}')`; + } catch { + return ''; + } +}; + const ProfileLayout = ({ children }: { children: ReactNode }) => { const { user } = useUserClient(); const { t } = useTranslation('common_blog'); @@ -105,15 +120,7 @@ const ProfileLayout = ({ children }: { children: ReactNode }) => { >
{ + try { + if (!profileData?.posting_json_metadata) return ''; + const metadata = JSON.parse(profileData.posting_json_metadata); + const coverImage = metadata?.profile?.cover_image; + + if (!coverImage || !isSafeImageUrl(coverImage)) return ''; + + const proxifiedUrl = proxifyImageSrc(coverImage, 2048, 512); + return `url('${escapeCssUrl(proxifiedUrl.replace(/ /g, '%20'))}')`; + } catch { + return ''; + } +}; + const ProfileLayout = ({ children }: IProfileLayout) => { const { t } = useTranslation('common_wallet'); const router = useRouter(); @@ -48,15 +64,9 @@ const ProfileLayout = ({ children }: IProfileLayout) => { {profileData ? (