diff --git a/components/Profile/index.tsx b/components/Profile/index.tsx index 29e506cc..526df2b8 100644 --- a/components/Profile/index.tsx +++ b/components/Profile/index.tsx @@ -1,7 +1,7 @@ import { ExplorerTooltip } from "@components/ExplorerTooltip"; import ShowMoreRichText from "@components/ShowMoreRichText"; import { EnsIdentity } from "@lib/api/types/get-ens"; -import { formatAddress } from "@lib/utils"; +import { formatAddress, sanitizeExternalUrl } from "@lib/utils"; import { Box, Button, @@ -67,6 +67,11 @@ const Index = ({ } }; + // ENS-supplied URL — must be validated before rendering as an external link + // so a malicious orchestrator can't smuggle in `javascript:` / `data:` / + // schemeless hrefs via their ENS `url` text record. + const safeIdentityUrl = sanitizeExternalUrl(identity?.url); + return ( - {identity?.url && ( + {safeIdentityUrl && ( - {identity.url.replace(/(^\w+:|^)\/\//, "")} + {safeIdentityUrl.replace(/(^\w+:|^)\/\//, "")} diff --git a/lib/utils.tsx b/lib/utils.tsx index 017f9382..5ae107d7 100644 --- a/lib/utils.tsx +++ b/lib/utils.tsx @@ -282,6 +282,37 @@ export const isImageUrl = (url: string): boolean => { return /\.(jpg|jpeg|png|gif|webp)$/i.test(url); }; +/** + * Sanitize a user-supplied URL for use as an external ``. + * + * Auto-prefixes `https://` when the input has no scheme (so values like + * `evil.com/path` aren't treated as relative links), then validates that the + * resulting URL parses and uses an `http:` or `https:` protocol. Anything + * else (e.g. `javascript:`, `data:`, malformed input) is rejected. + * + * @param url - The user-supplied URL. + * @returns The sanitized absolute URL, or `null` if it is unsafe / invalid. + */ +export const sanitizeExternalUrl = ( + url: string | null | undefined +): string | null => { + if (!url) return null; + const trimmed = url.trim(); + if (!trimmed) return null; + const withScheme = /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(trimmed) + ? trimmed + : `https://${trimmed}`; + try { + const parsed = new URL(withScheme); + if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { + return null; + } + return parsed.toString(); + } catch { + return null; + } +}; + /** * Shorten an Ethereum address for display. * @param address - The address to shorten.