mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 01:45:10 +03:00
Closes DOCS-1319 Part 4 of 4 in stack #50823. This PR carries **additions**: content the page never had. Style, structure, and snippet fixes land below it in #50820, #50821, and #50822. ## Problem Developers and AI agents read the Database functions guide. The guide shows how to write a function inside the database. A function runs in one of two modes. In `invoker` mode it runs as the caller. In `definer` mode it runs as the creator. The guide gives one rule for `definer` mode. The rule is to set the `search_path`. A reader who obeys that rule still gets an unsafe function. I wrote a function that obeys the rule. I pinned the search path to the empty string. Then I called it three times. | Caller | Result | | --- | --- | | The order's owner | 4330 cents, correct | | A different signed-in customer | 8660 cents, another customer's order | | Nobody, no session at all | 8660 cents | Every role can call a new function in `public`. The guide states that fact under Function privileges. It never connects the fact to the `definer` rule. A reader has no reason to look. Customers report the same failure. AI tools choose `definer` mode. The function then answers the front end with no session. The hole is hard to find, because no policy is involved in it. ## Solution - **Joins the two halves in the Security definer subsection.** A `danger` admonition states three facts: - The function runs with its creator's privileges. - A function created in the Dashboard or by a migration is owned by `postgres`, which bypasses Row Level Security. - Every role can call the function by default. The admonition then gives the fix. Check ownership inside the function body, and narrow the execute privilege as well. - **Applies the regrant to both ways of restricting execute.** The `grant execute` block sat inside the second way. A reader who took the first way saw no way to restore their own app's access. **Not changed:** the existing definer paragraph, the page structure, and the Function privileges statements. The lower PRs in the stack own those. **No eval re-run.** The guide scored 6 of 6 on three baseline runs, so the score has no room to move. All three runs chose `invoker` mode. The eval never enters the branch this PR fixes. Measuring it needs a new check, not a re-run. **Diff size:** one file, 15 lines added and 4 removed. ## Manual testing 1. Open the [Security definer vs invoker section](https://docs-git-docs-definer-function-privileges-supabase.vercel.app/docs/guides/database/functions#security-definer-vs-invoker) on the deploy preview. The admonition renders as a red `danger` panel below the definer paragraph. The two fixes appear as bullets. 2. Click the `Function privileges` link inside the admonition. It jumps to the [Function privileges section](https://docs-git-docs-definer-function-privileges-supabase.vercel.app/docs/guides/database/functions#function-privileges) on the same page. 3. Read that section. The `grant execute` block sits after the numbered list. It applies to both ways of restricting execute. 4. Run `pnpm build:guides-markdown` from `apps/docs`. Read `apps/docs/public/markdown/guides/database/functions.md`. The admonition appears as a `Danger:` paragraph. Discard the `manifest.json` change. 5. Run `npx prettier --check apps/docs/content/guides/database/functions.mdx`. Clean. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Documentation** * Expanded guidance on the security risks of `security definer` functions, including their privileges, interaction with Row Level Security, and default execution access. * Added recommendations for checking data ownership and restricting execution to intended roles. * Clarified that pinning `search_path` does not limit execution privileges. * Presented the function-privileges example separately from the default-privileges instructions. <!-- end of auto-generated comment: release notes by coderabbit.ai --> ## Preview links | Site | Live | Preview | Search for | | --- | --- | --- | --- | | Docs | [/docs/guides/database/functions](https://supabase.com/docs/guides/database/functions) | [/docs/guides/database/functions](https://docs-git-docs-definer-function-privileges-supabase.vercel.app/docs/guides/database/functions) | `every signed-in caller` | ## Review instructions This PR adds one admonition and moves one code block. The question is whether the admonition is correct and whether an agent reading the page would act on it. 1. Open the preview at [Security definer vs invoker](https://docs-git-docs-definer-function-privileges-supabase.vercel.app/docs/guides/database/functions#security-definer-vs-invoker). A red `danger` panel sits below the definer paragraph. 2. **Read the first paragraph for accuracy.** It claims the function runs with its creator's privileges, that a function created in the Dashboard or by a migration is owned by `postgres`, and that `postgres` bypasses Row Level Security. This matches [Use security definer functions](https://supabase.com/docs/guides/database/postgres/row-level-security#use-security-definer-functions) in the RLS guide. Flag any drift. 3. **Read the two bullets for sufficiency.** The ownership check is the primary fix. The execute grant is listed as a complement, not an alternative, because granting to `authenticated` still returns any user's row to every signed-in caller. Confirm the wording can't be read as "either one is enough." 4. Click the `Function privileges` link inside the admonition. It jumps down the same page. 5. In the Function privileges section, confirm the `grant execute` block sits after the numbered list rather than inside item 2, so it applies to both ways of restricting execute. 6. **Check the agent-facing copy.** Open [the markdown export](https://docs-git-docs-definer-function-privileges-supabase.vercel.app/docs/guides/database/functions.md) and find `Danger:`. This is what an agent reads, and it is the audience this PR exists for. **If you only have two minutes:** do steps 3 and 6. Step 3 is the correctness of the advice. Step 6 is whether the audience that prompted the ticket actually receives it. **A note on running SQL from this page.** Don't hand-paste from the rendered page. Blocks are split across tabs, and the Data tab in Returning data sets holds markdown tables that look pasteable but are not SQL. Use the `.md` export of the page, which flattens every tab in page order. #50822 has a copy-paste command for this.