Skip to content

Stop bookmarks and replies from copying a message the user cannot read #410

Description

@HMarzban

Summary

Two DEFINER paths return or copy a message's text using only its id:

  1. Bookmarks. The message_bookmarks INSERT policy checks only user_id. A user can bookmark any message_id. get_user_bookmarks (DEFINER) then returns that message's text with no channel check.
  2. Reply previews. The BEFORE trigger set_replied_message_preview (DEFINER) copies the preview of any reply_to_message_id into the new message, from any channel. The reply notification trigger also reads the parent by id only. So a reply posted in the user's own channel notifies the author of any message on the platform. The notice goes by push and email, with the replier's chosen display name.
  • Severity: Low for the read paths today, because every channel is PUBLIC. It becomes Medium once Private chat is gated (see the Private chat issue): these paths would bypass that gate. Today, anyone can use the cross-channel reply notification to send spam and phishing.
  • Area: Supabase
  • Source: security review of 2026-10-06

Production check (2026-10-06, read-only)

  • get_user_bookmarks is SECURITY DEFINER and executable by anon and authenticated. Its body uses auth.uid() but has no can_read_channel or channel_members check.
  • message_bookmarks policies: INSERT WITH CHECK (auth.uid() = user_id) only.
  • set_replied_message_preview is SECURITY DEFINER.

Where

  • Bookmark RPC: packages/supabase/migrations/20260625150000_bookmark_tab_scoped_list_rpc.sql.
  • Bookmark table policy: packages/supabase/scripts/06-message-bookmarks.sql:57-60. The RPC path checks visibility (packages/supabase/scripts/07-bookmark-functions.sql:36-56); the table path does not.
  • Reply preview trigger: packages/supabase/migrations/20260623120000_chat_media_attachments.sql:496-521, 563.
  • Reply notification trigger: same file :642-705.

Fix plan

  1. One reply guard. In set_replied_message_preview, raise an error when the parent message's channel_id differs from NEW.channel_id. No real UI path replies across channels. This one guard closes the preview copy, the metadata.replied copy and the cross-channel reply notification.
  2. Bookmarks.
    • Revoke INSERT, UPDATE and DELETE on message_bookmarks from authenticated and anon. The webapp uses only the bookmark RPCs (confirm with grep -rn "message_bookmarks" apps/webapp/src).
    • In get_user_bookmarks, join through internal.can_read_channel(m.channel_id) so a bookmark of an unreadable message returns nothing.
  3. Mirror in packages/supabase/scripts/, then run bun run --filter @docs.plus/supabase_back types.

Acceptance criteria

  • A reply whose parent is in another channel is refused.
  • A normal reply in the same channel still shows its preview and notifies the parent author.
  • A direct POST /rest/v1/message_bookmarks is refused. Bookmarking through the UI still works.
  • get_user_bookmarks does not return a message the caller cannot read.

Generated by Claude Code

Activity

  1. added theissue type on Oct 6, 2026
  2. changed the title [-][Security] Bookmarks and reply previews copy any message by id without a channel read check[/-] [+]Stop bookmarks and replies from copying a message the user cannot read[/+] on Oct 6, 2026
  3. added
    ChatRelated to chat features
    SecuritySecurity, access control, and data exposure
    on Oct 6, 2026
  4. HMarzban commented on Oct 6, 2026

    @HMarzban
    CollaboratorAuthor

    Status: fixed in 335138d on claude/youthful-lovelace-3nvsc6, in migration packages/supabase/migrations/20261006130000_close_client_write_and_grant_gaps.sql. Not merged or applied yet.

    What shipped:

    • Step 1. set_replied_message_preview raises SQLSTATE 22023 when the parent's channel_id differs from NEW.channel_id. This channel check also reads a soft-deleted parent, to match create_reply_notification. A parent id that does not exist still gets the "not available" preview.
    • Step 2. revoke insert, update, delete on public.message_bookmarks from authenticated, anon. get_user_bookmarks adds internal.can_read_channel(m.channel_id). Its signature and return table are unchanged.
    • Step 3. Mirrored in packages/supabase/scripts/07-bookmark-functions.sql, 10-5-func-replied_msg.sql and 13-RLS.sql. seed.sql is regenerated. No types change was needed.

    Left as is:

    • The write policies in packages/supabase/scripts/06-message-bookmarks.sql stay. Without the grant they have no effect.
    • Replies already stored across channels are not changed.
    • No automated test was added.

    Deploy: Supabase SQL has no deploy pipeline, so apply this migration by hand. It also carries #397, #401 and #409.

    Check after deploy: a reply in the same channel still shows its preview and notifies the parent's author. Bookmarking through the UI still works. This query returns false:

    select has_table_privilege('authenticated', 'public.message_bookmarks', 'INSERT');

    Generated by Claude Code

  5. HMarzban commented on Oct 9, 2026

    @HMarzban
    CollaboratorAuthor

    Reopened: the board automation closed this issue before the push. The code is on main through merge 083f37d84. It stays open until migration 20261006130000_close_client_write_and_grant_gaps.sql is applied on prod.

  6. HMarzban commented on Oct 9, 2026

    @HMarzban
    CollaboratorAuthor

    Deployed 2026-10-09 (run 37906965365, deploy job finished 09:28:41Z). Migration 20261006130000_close_client_write_and_grant_gaps.sql is applied on prod. Checked read-only: authenticated has no INSERT on workspaces.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    ChatRelated to chat featuresSecuritySecurity, access control, and data exposurebugSomething isn't working

    Type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions