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.
This commit is contained in:
Moheeb Zara
2026-05-19 21:49:51 -07:00
parent bcbe86424d
commit ef34089e57
2 changed files with 15 additions and 3 deletions
+8
View File
@@ -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)
})
})
+7 -3
View File
@@ -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:'
}