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.