From de42623c03e3ff1ffdea99335581fc0a67eae1f8 Mon Sep 17 00:00:00 2001 From: chappie-daemon Date: Thu, 15 Oct 2026 08:42:19 -0400 Subject: [PATCH 1/3] fix(settings): validate info.relay_url is a usable absolute URL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A relay_url like nostream.example.com (no scheme) passed validateSettings(), then broke every HTTP request: the CSP middleware in web-app-factory runs new URL(settings.info.relay_url) uncaught on every request, so /, /healthz and the whole Admin Console returned 500 TypeError: Invalid URL — with no way to fix it from the UI. validateSettings() now rejects a relay_url that is not a valid absolute URL, has no host (scheme-only strings like localhost:8008 parse with an empty host), or whose protocol is not ws/wss/http/https. Found and diagnosed by @tarunagnihotri534 in #824; the middleware omission and the new URL('localhost:8008') host quirk are his analysis. Closes #824 --- src/utils/settings-config.ts | 15 ++++++++ test/unit/utils/settings-config.spec.ts | 48 +++++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/src/utils/settings-config.ts b/src/utils/settings-config.ts index 93fb91498..ee3f2aba9 100644 --- a/src/utils/settings-config.ts +++ b/src/utils/settings-config.ts @@ -594,6 +594,21 @@ export const validateSettings = (settings: Settings): ValidationIssue[] => { if (!settings.info?.relay_url) { issues.push({ path: 'info.relay_url', message: 'relay_url is required' }) + } else if (typeof settings.info.relay_url === 'string') { + try { + const parsed = new URL(settings.info.relay_url) + + if (!parsed.host) { + issues.push({ path: 'info.relay_url', message: 'relay_url must include a valid host' }) + } else if (!['ws:', 'wss:', 'http:', 'https:'].includes(parsed.protocol)) { + issues.push({ + path: 'info.relay_url', + message: 'relay_url must use ws, wss, http, or https protocol', + }) + } + } catch { + issues.push({ path: 'info.relay_url', message: 'relay_url must be a valid absolute URL' }) + } } if (!settings.info?.name) { diff --git a/test/unit/utils/settings-config.spec.ts b/test/unit/utils/settings-config.spec.ts index d9179f35d..790f641c5 100644 --- a/test/unit/utils/settings-config.spec.ts +++ b/test/unit/utils/settings-config.spec.ts @@ -98,6 +98,54 @@ describe('settings-config', () => { expect(issues.some((issue) => issue.path === 'network')).to.equal(true) }) + it('accepts a relay_url with a valid ws/wss/http/https absolute URL', () => { + for (const relay_url of [ + 'wss://nostream.example.com', + 'ws://nostream.example.com:8008', + 'https://nostream.example.com', + 'http://localhost:8008', + ]) { + const issues = validateSettings({ info: { relay_url, name: 'test' } } as any) + + expect( + issues.filter((issue) => issue.path === 'info.relay_url'), + `expected ${relay_url} to pass`, + ).to.be.empty + } + }) + + it('rejects a relay_url that is not a valid absolute URL', () => { + const issues = validateSettings({ info: { relay_url: 'nostream.example.com', name: 'test' } } as any) + + expect( + issues.some( + (issue) => issue.path === 'info.relay_url' && issue.message === 'relay_url must be a valid absolute URL', + ), + ).to.equal(true) + }) + + it('rejects a scheme-only relay_url with no host', () => { + const issues = validateSettings({ info: { relay_url: 'localhost:8008', name: 'test' } } as any) + + expect( + issues.some( + (issue) => issue.path === 'info.relay_url' && issue.message === 'relay_url must include a valid host', + ), + ).to.equal(true) + }) + + it('rejects a relay_url whose protocol is not ws/wss/http/https', () => { + const issues = validateSettings({ info: { relay_url: 'ftp://nostream.example.com', name: 'test' } } as any) + + expect( + issues.some( + (issue) => + issue.path === 'info.relay_url' && + issue.message === 'relay_url must use ws, wss, http, or https protocol', + ), + ).to.equal(true) + }) + describe('rate limit period validation', () => { const baseSettings = () => ({ From 22540e41fca36d0dbd57c6854e57136c19070d64 Mon Sep 17 00:00:00 2001 From: chappie-daemon Date: Thu, 15 Oct 2026 08:45:32 -0400 Subject: [PATCH 2/3] chore: add changeset for relay_url validation fix --- .changeset/validate-relay-url.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/validate-relay-url.md diff --git a/.changeset/validate-relay-url.md b/.changeset/validate-relay-url.md new file mode 100644 index 000000000..e820f851e --- /dev/null +++ b/.changeset/validate-relay-url.md @@ -0,0 +1,7 @@ +--- +"nostream": patch +--- + +fix(settings): validate `info.relay_url` is a usable absolute URL + +`validateSettings()` now rejects a `relay_url` that is not a valid absolute URL, parses with an empty host (scheme-only strings like `localhost:8008`), or uses a protocol other than `ws`/`wss`/`http`/`https`. Previously an invalid value started cleanly and then broke every HTTP request with `500 TypeError: Invalid URL`, including the Admin Console, leaving the bad setting fixable only by editing the file by hand. Found in #824. From 15a97d3a093014a5fcf86fca88950fe85fdf8101 Mon Sep 17 00:00:00 2001 From: chappie-daemon Date: Thu, 15 Oct 2026 08:49:14 -0400 Subject: [PATCH 3/3] test(settings): cover every allowed relay_url protocol The accept test now exercises ws, wss, http and https each with and without an explicit port, plus IPv6 and bare-IP hosts. The reject-protocol test adds gopher (opaque port) and whttp (unknown scheme) so all four allowed protocols are pinned against near-miss neighbours, not just one counterexample. --- test/unit/utils/settings-config.spec.ts | 31 +++++++++++++++++-------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/test/unit/utils/settings-config.spec.ts b/test/unit/utils/settings-config.spec.ts index 790f641c5..dc0fe08b5 100644 --- a/test/unit/utils/settings-config.spec.ts +++ b/test/unit/utils/settings-config.spec.ts @@ -100,10 +100,14 @@ describe('settings-config', () => { it('accepts a relay_url with a valid ws/wss/http/https absolute URL', () => { for (const relay_url of [ - 'wss://nostream.example.com', 'ws://nostream.example.com:8008', - 'https://nostream.example.com', + 'wss://nostream.example.com', 'http://localhost:8008', + 'http://nostream.example.com', + 'https://nostream.example.com', + 'https://nostream.example.com:8443', + 'ws://[::1]:8008', + 'wss://127.0.0.1', ]) { const issues = validateSettings({ info: { relay_url, name: 'test' } } as any) @@ -135,15 +139,22 @@ describe('settings-config', () => { }) it('rejects a relay_url whose protocol is not ws/wss/http/https', () => { - const issues = validateSettings({ info: { relay_url: 'ftp://nostream.example.com', name: 'test' } } as any) + for (const relay_url of [ + 'ftp://nostream.example.com', + 'gopher://nostream.example.com:70', + 'whttp://nostream.example.com', + ]) { + const issues = validateSettings({ info: { relay_url, name: 'test' } } as any) - expect( - issues.some( - (issue) => - issue.path === 'info.relay_url' && - issue.message === 'relay_url must use ws, wss, http, or https protocol', - ), - ).to.equal(true) + expect( + issues.some( + (issue) => + issue.path === 'info.relay_url' && + issue.message === 'relay_url must use ws, wss, http, or https protocol', + ), + `expected ${relay_url} to be rejected for protocol`, + ).to.equal(true) + } }) describe('rate limit period validation', () => {