Skip to content

fix(db): re-lock SECURITY DEFINER functions lost in the dev2 move - #341

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

ralyodio merged 1 commit into
masterfrom
sec/secdef-lockdown

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

What

The dev2 restore dropped the ACLs from #247, so all 53 SECURITY DEFINER functions in public were executable by the public anon key again. The #247 bodies treat auth.uid() IS NULL as "trusted server caller", so anon passed every check.

Worst cases (anon key, no sign-in):

  • upsert_user_public_key(target, key): replace any user's public key (silent E2EE MITM).
  • fn_get_user_message_content, get_user_call_sessions, fn_get_user_active_deliveries, get_user_call_history, get_user_active_calls, get_conversations_with_archive_*, get_user_archived_conversations: other users' messages envelopes, calls, deliveries, conversation lists.
  • sync_user_with_auth, fix_*, update_canonical_auth_user_ids, nuclear_delete_user_data, delete_encrypted_data_only, log_sms_event, get_sms_stats, cleanup functions: internal.

Change (migration 20261026210000, already applied by hand on dev2)

  • Re-asserts the fix(security): revoke anon EXECUTE on the remaining 41 SECURITY DEFINER fns #247 grant matrix. No function bodies changed (messaging behaviour identical). 33 internal functions -> service_role only; 20 user-callable ones (all scoped to auth.uid()) -> authenticated + service_role, never anon. fn_cleanup_expired_messages moved to service_role (app only calls it with the service role).
  • users.account_type and users.operator_user_id added to enforce_users_immutable_columns: an anonymous account could PATCH itself to verified and get the free daily phone minutes.
  • blog_posts_bak_20261014: RLS on, anon/authenticated grants revoked.
  • key_access_log / sms_notifications "System can insert" policies (auth.uid() IS NULL) now TO service_role (they matched anon).
  • Default privileges: new functions not executable by PUBLIC/anon.

For Preshy (messaging)

Nothing in the message/key/call code paths changed. If you add a new RPC that the browser calls, grant it explicitly: GRANT EXECUTE ON FUNCTION ... TO authenticated; (anon no longer gets new functions by default). SECURITY DEFINER functions should check auth.uid() and must not treat NULL as trusted unless anon is revoked.

Verified live

  • anon: get_sms_stats, upsert_user_public_key, get_user_public_key -> 42501 permission denied.
  • throwaway signed-in user: upsert_user_public_key for another user -> Access denied; own key OK; get_user_conversations_enhanced, update_user_activity, fn_get_user_active_deliveries, fn_mark_read, get_user_active_calls, get_user_call_history, find_user_by_unique_identifier, is_conversation_participant all 200/204; get_user_call_sessions, fn_get_user_message_content -> 42501; PATCH account_type -> rejected, PATCH bio OK. Real traffic update_user_activity 204s after apply. Test users deleted.

🤖 Generated with Claude Code

…n the dev2 move

All 53 SECURITY DEFINER functions in public were executable by the anon key again
(the dev2 restore dropped the #247 ACLs). Because the #247 bodies treat a NULL
auth.uid() as the trusted server, anon walked through every check, e.g.
upsert_user_public_key could replace any user's public key.

Re-asserts the #247 grant matrix (bodies unchanged), pins users.account_type and
operator_user_id (anonymous -> verified self-upgrade unlocked free phone minutes),
locks blog_posts_bak_20261014, scopes the 'system' insert policies to service_role,
and stops new functions defaulting to PUBLIC/anon execute. Applied by hand on dev2.

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

Copy link
Copy Markdown

ThreatCrush Security Scan

17 finding(s)

MEDIUM: 13 | LOW: 4

Severity Rule Location
MEDIUM js-unescaped-html-sink src/app/blog/[slug]/page.jsx:38
MEDIUM js-unescaped-html-sink src/app/blog/[slug]/page.jsx:66
MEDIUM js-unescaped-html-sink src/app/faq/page.jsx:57
MEDIUM js-unescaped-html-sink src/app/layout.jsx:142
MEDIUM js-unescaped-html-sink src/app/layout.jsx:146
MEDIUM js-unescaped-html-sink src/app/page.jsx:50
MEDIUM js-open-redirect src/app/phone/page.jsx:70
MEDIUM redos-nested-quantifier src/lib/auth/dns-name.js:88
MEDIUM redos-nested-quantifier src/lib/chat/voice-mode.js:116
MEDIUM redos-nested-quantifier src/lib/chat/voice-mode.js:121
MEDIUM js-unescaped-html-sink src/lib/components/chat/MessageItem.jsx:85
MEDIUM js-unescaped-html-sink src/lib/components/chat/MessageItem.jsx:130
MEDIUM js-unescaped-html-sink src/lib/components/PitFooter.jsx:15
LOW secret-generic-credential src/app/api/auth/register-anon/route.test.js:32
LOW secret-jwt tests/debug-sms.js:10
LOW secret-generic-credential tests/private-key-import-export.test.js:252
LOW secret-generic-credential tests/private-key-import-export.test.js:264

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit 943bf19 into master Oct 26, 2026
11 checks passed
@ralyodio
ralyodio deleted the sec/secdef-lockdown branch October 26, 2026 20:35
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