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:' }