From 249bc08fc5b4bc67b7f017da556710a8aea0ffcb Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Wed, 29 Apr 2026 12:33:27 +0200 Subject: [PATCH] fix: validate identity.url scheme before rendering as external link MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Orchestrator profile URLs come from ENS text records — a user-controlled field. As rendered, values like `javascript:alert(...)`, `data:...`, or schemeless `evil.com/path` (treated as a relative URL) would all become clickable from the profile page. React 16+ warns on `javascript:` hrefs but doesn't reliably block them. Add `sanitizeExternalUrl` in `lib/utils.tsx` that auto-prefixes `https://` for schemeless input, parses via `new URL(...)`, and rejects anything outside `http:` / `https:`. Use the sanitized value for both `href` and the displayed text; suppress the entire link block when sanitization fails. Co-Authored-By: Claude Opus 4.7 (1M context) --- components/Profile/index.tsx | 15 ++++++++++----- lib/utils.tsx | 31 +++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 5 deletions(-) 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.