Skip to content

security: pin managed-number + org plan columns, default privileges, tab-redirect bypass - #112

Merged
ralyodio merged 1 commit into
masterfrom
security/secdef-lockdown
Oct 26, 2026
Merged

ralyodio merged 1 commit into
masterfrom
security/secdef-lockdown

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

From the 2026-10-26 fleet SECURITY DEFINER audit (smshub-dev-supabase-db).

Functions: 4 exposed, 0 dangerous. is_org_admin, is_org_member, is_team_member and is_team_owner only answer for auth.uid(), and the RLS policies use them, so they keep their grants. Default privileges no longer grant EXECUTE on new functions to anon/authenticated, and trigger functions are revoked.

Privileged columns (rule 10):

  • phone_numbers.telnyx_number_id could be rewritten by the owner of a managed number. The expiry job (service.ts) releases whatever Telnyx id the row carries, so pointing a lapsing number at another customer's id would release their number. That column and released_at are now pinned, and so are the existing fields. A provider_id change must name one of the caller's own providers.
  • Direct inserts into phone_numbers via PostgREST skipped everything POST /api/phone-numbers checks: provider ownership, the BYO plan limit and the "already connected" check. The API already inserts with the service role, so direct inserts are now refused. Friendly-name edits through the user client still work.
  • organizations.plan was writable by org admins. It is display only, because the real plan comes from managed numbers, but it is now pinned to the server.

Open redirect: safeNextPath accepted /\t/evil.example. The URL parser strips the tab, which leaves //evil.example. So /auth/callback?next=/%09/evil.example and the login page's router.push(next) both went off-site. Control characters and backslashes are now refused, and the result must resolve same-origin. Tests added.

Applied to prod by hand after a schema backup and a rolled-back dry run (simulated authenticated user: friendly_name update ok, telnyx id change refused, direct insert refused), followed by notify pgrst.

Not fixed, functional bug only: creating an org from /org can't work. The client inserts into organizations and then into org_members, and both policies require is_org_admin of an org that has no members yet. There are 0 orgs in prod.

Tests: vitest 52 files / 320 passed, tsc clean.

🤖 Generated with Claude Code

…CUTE, close tab-redirect bypass

Migration applied to prod by hand.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

12 finding(s)

HIGH/CRITICAL: 2 | MEDIUM: 4 | LOW: 6

Severity Rule Location
HIGH sh-unquoted-expansion-destructive public/install.sh:67
HIGH sh-remote-script-execution public/install.sh:89
MEDIUM sh-remote-script-execution public/install.sh:133
MEDIUM js-open-redirect src/app/numbers/page.tsx:180
MEDIUM js-open-redirect src/components/inbox-client.tsx:35
MEDIUM js-unescaped-html-sink src/components/pit-footer.tsx:17
LOW secret-generic-credential src/lib/providers/__tests__/twilio.test.ts:24
LOW secret-generic-credential src/lib/providers/__tests__/twilio.test.ts:46
LOW secret-generic-credential src/lib/providers/__tests__/twilio.test.ts:60
LOW secret-generic-credential src/lib/providers/__tests__/twilio.test.ts:78
LOW secret-generic-credential supabase/config.toml:287
LOW secret-generic-credential supabase/config.toml:319

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit a5c6778 into master Oct 26, 2026
12 checks passed
@ralyodio
ralyodio deleted the security/secdef-lockdown branch October 26, 2026 21:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant