From c6500d6b7ac589aba09c6d9ae7c7dde38ae2f17f Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Fri, 17 Oct 2025 10:03:50 -0400 Subject: [PATCH] Improve channel banner sanitization --- js/channelProfile.js | 68 ++++++-------------------------- js/state/cache.js | 14 ++++++- js/utils/profileMedia.js | 80 ++++++++++++++++++++++++++++++++++++++ views/channel-profile.html | 6 ++- 4 files changed, 108 insertions(+), 60 deletions(-) create mode 100644 js/utils/profileMedia.js diff --git a/js/channelProfile.js b/js/channelProfile.js index 7efbf85f..7e8fd071 100644 --- a/js/channelProfile.js +++ b/js/channelProfile.js @@ -15,6 +15,7 @@ import createPopover from "./ui/overlay/popoverEngine.js"; import { VideoCard } from "./ui/components/VideoCard.js"; import { createChannelProfileMenuPanel } from "./ui/components/videoMenuRenderers.js"; import { ALLOW_NSFW_CONTENT } from "./config.js"; +import { sanitizeProfileMediaUrl } from "./utils/profileMedia.js"; import { calculateZapShares, describeShareType, @@ -2097,73 +2098,26 @@ function trimProfileString(value) { return value.trim(); } -function normalizeProfileMediaUrl(value) { - const trimmed = trimProfileString(value); - if (!trimmed) { - return ""; - } - - if (/^data:image\//i.test(trimmed)) { - return trimmed; - } - - if (/^blob:/i.test(trimmed)) { - return trimmed; - } - - if (/^https?:\/\//i.test(trimmed)) { - return trimmed; - } - - if (trimmed.startsWith("//")) { - return `https:${trimmed}`; - } - - if (/^ipfs:\/\//i.test(trimmed)) { - const ipfsPath = trimmed - .replace(/^ipfs:\/\//i, "") - .replace(/^ipfs\//i, ""); - if (!ipfsPath) { - return ""; - } - return `https://ipfs.io/ipfs/${encodeURI(ipfsPath)}`; - } - - if (/^(?:\.\.\/|\.\/|\/)/.test(trimmed)) { - return trimmed; - } - - if (/^assets\//i.test(trimmed)) { - return trimmed; - } - - const localhostPattern = /^(?:localhost|(?:\d{1,3}\.){3}\d{1,3})(?::\d+)?(?:[/?#].*)?$/i; - if (localhostPattern.test(trimmed)) { - return `http://${trimmed}`; - } - - const domainPattern = - /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9-]{2,}(?::\d+)?(?:[/?#].*)?$/i; - if (domainPattern.test(trimmed)) { - return `https://${trimmed}`; - } - - return ""; -} - function normalizeChannelProfileMetadata(raw = {}) { const name = trimProfileString(raw.display_name) || trimProfileString(raw.name) || "Unknown User"; const picture = - normalizeProfileMediaUrl(raw.picture) || - normalizeProfileMediaUrl(raw.image) || + sanitizeProfileMediaUrl(raw.picture) || + sanitizeProfileMediaUrl(raw.image) || FALLBACK_CHANNEL_AVATAR; const about = trimProfileString(raw.about || raw.bio); const website = trimProfileString(raw.website || raw.url); const banner = - normalizeProfileMediaUrl(raw.banner || raw.header || raw.background) || ""; + sanitizeProfileMediaUrl( + raw.banner || + raw.header || + raw.background || + raw.cover || + raw.cover_image || + raw.coverImage + ) || ""; const lud16 = trimProfileString(raw.lud16); const lud06 = trimProfileString(raw.lud06); const lightningAddress = diff --git a/js/state/cache.js b/js/state/cache.js index 52332ff6..e2a30e0a 100644 --- a/js/state/cache.js +++ b/js/state/cache.js @@ -1,4 +1,5 @@ import { userLogger } from "../utils/logger.js"; +import { sanitizeProfileMediaUrl } from "../utils/profileMedia.js"; import { readUrlHealthFromStorage, removeUrlHealthFromStorage, @@ -577,7 +578,9 @@ export function setProfileCacheEntry(pubkey, profile, { persist = true } = {}) { const normalized = { name: profile.name || profile.display_name || "Unknown", - picture: profile.picture || "assets/svg/default-profile.svg", + picture: + sanitizeProfileMediaUrl(profile.picture || profile.image) || + "assets/svg/default-profile.svg", }; const about = sanitizeProfileString(profile.about || profile.aboutMe); @@ -590,7 +593,14 @@ export function setProfileCacheEntry(pubkey, profile, { persist = true } = {}) { normalized.website = website; } - const banner = sanitizeProfileString(profile.banner || profile.header); + const banner = sanitizeProfileMediaUrl( + profile.banner || + profile.header || + profile.background || + profile.cover || + profile.cover_image || + profile.coverImage + ); if (banner) { normalized.banner = banner; } diff --git a/js/utils/profileMedia.js b/js/utils/profileMedia.js new file mode 100644 index 00000000..07cb2dfd --- /dev/null +++ b/js/utils/profileMedia.js @@ -0,0 +1,80 @@ +import { devLogger } from "./logger.js"; + +const LOCALHOST_PATTERN = /^(?:localhost|(?:\d{1,3}\.){3}\d{1,3})(?::\d+)?(?:[/?#].*)?$/i; +const DOMAIN_PATTERN = /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9-]{2,}(?::\d+)?(?:[/?#].*)?$/i; + +function normalizeIpfsGatewayUrl(trimmed) { + const ipfsPath = trimmed.replace(/^ipfs:\/\//i, "").replace(/^ipfs\//i, ""); + if (!ipfsPath) { + return ""; + } + return `https://ipfs.io/ipfs/${encodeURI(ipfsPath)}`; +} + +function coerceHttpToHttps(url) { + if (!/^http:\/\//i.test(url)) { + return url; + } + + if (LOCALHOST_PATTERN.test(url.replace(/^http:\/\//i, ""))) { + return url; + } + + return `https://${url.slice(7)}`; +} + +export function sanitizeProfileMediaUrl(value) { + if (typeof value !== "string") { + return ""; + } + + let trimmed = value.trim(); + if (!trimmed) { + return ""; + } + + trimmed = trimmed.replace(/^['"]+|['"]+$/g, ""); + if (!trimmed) { + return ""; + } + + if (/^data:image\//i.test(trimmed)) { + return trimmed; + } + + if (/^blob:/i.test(trimmed)) { + return trimmed; + } + + if (/^ipfs:\/\//i.test(trimmed)) { + return normalizeIpfsGatewayUrl(trimmed); + } + + if (trimmed.startsWith("//")) { + trimmed = `https:${trimmed}`; + } + + if (/^(?:\.\.\/|\.\/|\/)/.test(trimmed)) { + return trimmed; + } + + if (/^assets\//i.test(trimmed)) { + return trimmed; + } + + if (!/^https?:\/\//i.test(trimmed)) { + if (LOCALHOST_PATTERN.test(trimmed)) { + trimmed = `http://${trimmed}`; + } else if (DOMAIN_PATTERN.test(trimmed)) { + trimmed = `https://${trimmed}`; + } else { + devLogger?.warn?.( + "[profileMedia.sanitizeProfileMediaUrl] Unsupported media URL", + trimmed + ); + return ""; + } + } + + return coerceHttpToHttps(trimmed); +} diff --git a/views/channel-profile.html b/views/channel-profile.html index e7e2bafc..ba77fd80 100644 --- a/views/channel-profile.html +++ b/views/channel-profile.html @@ -5,8 +5,12 @@ Banner