Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Access control

Question: Does a policy let every user it applies to reach other users’ rows, or trust a value users can change? Does a SECURITY DEFINER function leave search_path open or skip checking the caller? Does a grant open writes or private reads to every user? Does a public table hold users’ own data, a view return other users’ rows, or a reducer change rows its arguments choose, or admin-only settings, without checking the caller?

  • Runs: when selected: --rule access-control, --rule security or --rule all · Fails the check by default: no
  • Right on projects JevGate was never tuned on: reviews none labeled, considers none labeled (how it is measured)
  • Right on the projects it was tuned on: reviews 2 of 5, considers 5 of 14
  • Looks at: SQL files: row-level security policies, SECURITY DEFINER functions and grants, in their final state across migrations; SpacetimeDB TypeScript modules: public tables, views and reducers
  • Evidence unit: one policy with its table and the functions it calls, one SECURITY DEFINER function, or one grant; one SpacetimeDB public table with its user columns, or one view or reducer with the functions it calls and the framework version
  • Acceptable: Policies tied to the user, account or membership; role checks; restrictive policies; public data; grants narrowed by row-level security; reducers that check the caller through ctx.sender, the module owner, an admin or a trusted service identity, or run only on a schedule
  • Names: security/access-control, access-control, access_control · Version: 4

When a finding is right

A finding says a SQL policy, SECURITY DEFINER function or grant lets users reach other users’ rows, or that a SpacetimeDB table, view or reducer exposes or changes other users’ data without checking the caller. It is right for a policy that trusts user_metadata, a SECURITY DEFINER function every role may call that deletes any stored file, or a grant that opens writes to every user. It is wrong when the rows are ones their owners chose to share, or when the function answers only what every user may read already. Two thirds of the findings labeled wrong or debatable were policies showing rows their owners marked shared.

The rule has no labels on projects JevGate was never tuned on, so its levels cannot be measured yet.

Findings it got wrong

Labeled wrong by reading the code, on open-source projects the rules were tuned on.

Basejump: accept_invitation

  • Where: supabase/migrations/20240414162100_basejump-invitations.sql:158 in usebasejump/basejump at 7a1f95c.
  • Finding (review): SECURITY DEFINER function accept_invitation reads or changes other users’ rows without checking the caller.
  • Why it was wrong: The invitation token is the check: 30 random bytes, matched exactly and valid for a day. The function adds only the caller (auth.uid()) to the account, and only signed-in users may execute it.
  • Since: cleared in 0.20.0: a SECURITY DEFINER function that acts only for whoever holds a secret token it looks up by value does not skip the caller check (changelog).

Chatbot UI: shared files

  • Where: supabase/migrations/20240108234544_add_files.sql:44 in mckaywrigley/chatbot-ui at 81328b6.
  • Finding (consider): Policy allow view access to non-private files on files likely lets every user it applies to read or change other users’ rows.
  • Why it was wrong: Others can read a file only once its owner changes sharing from the default 'private', and only the owner can, through the policy on their own files. This is the read side of sharing; tying the condition to the user’s id would remove the feature.
  • Since: 0.21.0 accepts a policy that lets others read rows their owners marked shared, and the same policy on chats is no longer reported. This one is still a consider, now for trusting a value users can change, which is wrong too: only the owner can set sharing. Not addressed.

Chatbot UI: non_private_file_exists

  • Where: supabase/migrations/20240108234544_add_files.sql:92 in mckaywrigley/chatbot-ui at 81328b6.
  • Finding (review): SECURITY DEFINER function non_private_file_exists reads or changes other users’ rows without checking the caller.
  • Why it was wrong: The function returns only whether a file with that id exists with sharing <> 'private': exactly the rows the shared-files policy already shows every role. Private and missing files both give false. Its open search_path would be a fair consider; a missing caller check is not.
  • Since: not addressed; reported the same way from 0.20.0 through 0.25.0.