diff --git a/src/apps/relay-admin/PolicySections.tsx b/src/apps/relay-admin/PolicySections.tsx
index d79fff5..1f71367 100644
--- a/src/apps/relay-admin/PolicySections.tsx
+++ b/src/apps/relay-admin/PolicySections.tsx
@@ -18,6 +18,7 @@ import {
parsePubkeyInput,
validateIpInput,
validateReason,
+ type AllowedPubkey,
type BannedPubkey,
type BlockedIp,
type Nip86CoreMethod,
@@ -400,7 +401,7 @@ export function AllowedPubkeysSection({
/>
) : (
- {filtered.map((entry: BannedPubkey) => (
+ {filtered.map((entry: AllowedPubkey) => (
{
expect(sanitizeIconUrl('not a url')).toHaveProperty('error');
});
+ it('only allows http:// for local relays', () => {
+ expect(sanitizeIconUrl('http://localhost:4869/icon.png')).toBe('http://localhost:4869/icon.png');
+ expect(sanitizeIconUrl('http://127.0.0.1/icon.png')).toBe('http://127.0.0.1/icon.png');
+ expect(sanitizeIconUrl('http://relay.example.com/icon.png')).toHaveProperty('error');
+ });
+
it('caps reason length', () => {
expect(validateReason('spam')).toBeUndefined();
expect(validateReason('x'.repeat(501))).toBeTruthy();
diff --git a/src/lib/nip86.ts b/src/lib/nip86.ts
index 9dbad33..224dc5d 100644
--- a/src/lib/nip86.ts
+++ b/src/lib/nip86.ts
@@ -549,6 +549,12 @@ export function validateRoleColor(input: string): string | undefined {
: 'Use a hex color like #8b5cf6, or leave it empty.';
}
+/** Loopback/`.local` hostnames — the only ones http:// is trusted for below. */
+function isLocalHostname(hostname: string): boolean {
+ const host = hostname.toLowerCase();
+ return host === 'localhost' || host === '127.0.0.1' || host === '::1' || host.endsWith('.local');
+}
+
/**
* Validate and sanitize a relay icon URL before it is shown or submitted.
* Only https (and http for local relays) URLs survive — anything else could
@@ -559,8 +565,8 @@ export function sanitizeIconUrl(input: string): string | { error: string } {
if (!value) return { error: 'Enter an icon URL.' };
try {
const parsed = new URL(value);
- if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
- return { error: 'Only https:// icon URLs are allowed.' };
+ if (parsed.protocol !== 'https:' && !(parsed.protocol === 'http:' && isLocalHostname(parsed.hostname))) {
+ return { error: 'Only https:// icon URLs are allowed (http:// only for local relays).' };
}
return parsed.href;
} catch {