Skip to content

fix: validate identity.url scheme before rendering as external link#644

Open
rickstaa wants to merge 1 commit intomainfrom
fix/profile-url-href-validation
Open

fix: validate identity.url scheme before rendering as external link#644
rickstaa wants to merge 1 commit intomainfrom
fix/profile-url-href-validation

Conversation

@rickstaa
Copy link
Copy Markdown
Member

@rickstaa rickstaa commented Apr 29, 2026

Closes #643

Summary

Sanitizes the ENS-supplied identity.url value before rendering it as an external link in the orchestrator profile header. Adds sanitizeExternalUrl to lib/utils.tsx and uses it for both href and the displayed text.

Why

identity.url is set by the orchestrator via their ENS url text record — fully user-controlled. Without validation, values like javascript:alert(...), data:text/html,..., or schemeless evil.com/path (which resolves as a relative link to explorer.livepeer.org) are rendered verbatim into the DOM. React 16+ warns on javascript: hrefs but does not reliably block them.

What changed

  • Add sanitizeExternalUrl(url) helper to lib/utils.tsx. It auto-prefixes https:// for schemeless input, parses via new URL(...), returns the canonical form on success, or null for anything that fails to parse or uses a non-http(s) protocol.
  • In components/Profile/index.tsx, compute safeIdentityUrl once at the top of the component and use it for the link's href, title, and the visible text. Suppress the entire link block when sanitization fails.
  • Twitter / GitHub blocks are untouched — those interpolate handles into hardcoded https://twitter.com/ / https://github.com/ URLs and are not the same risk.

Test plan

  • pnpm typecheck passes (via pre-commit hook).
  • pnpm lint and prettier --check pass.
  • Reviewer: visit a profile with a valid http(s) URL — link still works as before.
  • Reviewer: confirm a profile with a javascript: URL doesn't render the link block at all.

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings April 29, 2026 10:34
@rickstaa rickstaa requested a review from ECWireless as a code owner April 29, 2026 10:34
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Apr 29, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
explorer-arbitrum-one Ready Ready Preview, Comment Apr 29, 2026 10:49am

Request Review

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Addresses a security issue where an ENS-supplied identity.url could be rendered directly into an external link without validation.

Changes:

  • Added sanitizeExternalUrl utility to validate/normalize user-supplied URLs to safe http(s) absolute URLs.
  • Updated the profile page to use the sanitized URL (or skip rendering the link when invalid).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
lib/utils.tsx Adds sanitizeExternalUrl for scheme normalization and protocol validation.
components/Profile/index.tsx Uses sanitizeExternalUrl(identity?.url) before rendering an external website link.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread components/Profile/index.tsx Outdated
Comment on lines 285 to 287
title={safeIdentityUrl}
target="__blank"
rel="noopener noreferrer"
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

target should be _blank (single underscore) to open a new tab/window. Using __blank doesn’t match the standard browsing context name and may not behave as intended.

Copilot uses AI. Check for mistakes.
Comment thread lib/utils.tsx
Comment on lines +302 to +307
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:") {
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sanitizeExternalUrl currently prepends https:// for any input without a detected scheme, but this produces incorrect results for protocol-relative URLs like //example.com/path (it becomes https:////example.com/path, which parses with an empty host and doesn’t reliably become an external link). Handle the //... case explicitly (e.g., prefix with https:) and consider rejecting other relative-leading inputs (like /..., #..., ?...) or requiring parsed.hostname to be non-empty to ensure the result is truly an absolute external URL.

Suggested change
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:") {
if (/^[/?#]/.test(trimmed) && !trimmed.startsWith("//")) return null;
const withScheme = trimmed.startsWith("//")
? `https:${trimmed}`
: /^[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:") ||
!parsed.hostname
) {

Copilot uses AI. Check for mistakes.
Comment thread lib/utils.tsx
Comment on lines +296 to +314
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;
}
};
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are existing Jest unit tests for lib/utils (see lib/utils.test.ts), but the new sanitizeExternalUrl behavior isn’t covered. Add focused tests for allowed inputs (http/https + schemeless hostnames that get https:// prefixed) and rejected inputs (javascript:, data:, malformed URLs, etc.) to prevent regressions.

Copilot uses AI. Check for mistakes.
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) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Triage

Development

Successfully merging this pull request may close these issues.

security: profile page renders unsanitized ENS-supplied URL

2 participants