From ef34089e57dd522447e49a87dfcd80f4793a2201 Mon Sep 17 00:00:00 2001 From: Moheeb Zara Date: Tue, 19 May 2026 21:49:51 -0700 Subject: [PATCH] harden: isSafeDuesUrl requires literal https:// so app and DB CHECK agree Edge URLs like https:example.com parse to protocol https: but lack //, which the migration-050 CHECK rejects -- the admin would see a confusing save failure instead of a clear validation error. Require the prefix so the Zod boundary matches the data layer. Fail-closed either way; no security impact. --- __tests__/dues-payments-logic.test.ts | 8 ++++++++ lib/dues-payments-logic.ts | 10 +++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/__tests__/dues-payments-logic.test.ts b/__tests__/dues-payments-logic.test.ts index 7606904..b6284e4 100644 --- a/__tests__/dues-payments-logic.test.ts +++ b/__tests__/dues-payments-logic.test.ts @@ -48,4 +48,12 @@ describe('isSafeDuesUrl', () => { expect(isSafeDuesUrl('paypal.me/x')).toBe(false) expect(isSafeDuesUrl('')).toBe(false) }) + + it('rejects https without // (stays in lockstep with the DB CHECK)', () => { + // these parse to protocol https: but lack the // the DB CHECK requires + expect(isSafeDuesUrl('https:example.com')).toBe(false) + expect(isSafeDuesUrl('https:/x')).toBe(false) + // bare scheme with no host + expect(isSafeDuesUrl('https://')).toBe(false) + }) }) diff --git a/lib/dues-payments-logic.ts b/lib/dues-payments-logic.ts index bdc1361..ec6907c 100644 --- a/lib/dues-payments-logic.ts +++ b/lib/dues-payments-logic.ts @@ -26,11 +26,15 @@ export function isDuesLinkPlatform(p: string): p is DuesLinkPlatform { // (javascript:, data:, mailto:, etc.) are rejected so an admin-entered value // cannot become an XSS or a downgrade vector when shown to members. export function isSafeDuesUrl(url: string): boolean { - let u: URL + // Require the literal https:// prefix AND a parseable URL. The prefix check + // keeps this in lockstep with the DB CHECK (`url ~* '^https://'`, migration + // 050) so the app and data layers never disagree (e.g. `https:example.com` + // parses to https: but lacks `//`, which the DB rejects); the parse check + // rejects a bare `https://` with no host. + if (!/^https:\/\//i.test(url)) return false try { - u = new URL(url) + return new URL(url).protocol === 'https:' } catch { return false } - return u.protocol === 'https:' }