security: enforce https on dues_payment_methods.url at the data layer

The url renders to members as a clickable href. https was enforced only
in the admin Zod action, but RLS lets an admin/board write the row
directly via PostgREST, bypassing Zod, with no DB constraint -- a
malicious admin could store a javascript: URL and XSS members who click.

- Migration 050: CHECK (url ~* '^https://') so the DB is the source of
  truth; integration-tested that the service path cannot store
  javascript:/http:.
- Member-read action filters with isSafeDuesUrl (defense-in-depth).
This commit is contained in:
Moheeb Zara
2026-05-19 21:35:49 -07:00
parent 5ce6b29d4a
commit f2534323da
7 changed files with 61 additions and 2 deletions
+18
View File
@@ -78,6 +78,24 @@ d('dues_payment_methods RLS', () => {
expect(r.ok).toBe(false)
})
it('the url CHECK rejects non-https schemes even via the service path (050)', () => {
const sid = space()
// service path bypasses RLS + the app Zod check, so the DB CHECK is the
// last line of defense against a stored javascript:/http: href.
const jsUrl = execService(
`insert into dues_payment_methods (space_id, platform, url) values ('${sid}','paypal','javascript:alert(1)');`,
)
expect(jsUrl.ok).toBe(false)
const httpUrl = execService(
`insert into dues_payment_methods (space_id, platform, url) values ('${sid}','venmo','http://insecure');`,
)
expect(httpUrl.ok).toBe(false)
const httpsUrl = execService(
`insert into dues_payment_methods (space_id, platform, url) values ('${sid}','zeffy','https://ok.example');`,
)
expect(httpsUrl.ok).toBe(true)
})
it('an unverified member cannot write even with admin role (046 status gate)', () => {
const sid = space()
const pending = seedMember(sid, { role: 'admin', status: 'unverified' })