VibeRaven

What do the Supabase Security Advisor warnings mean, and how do I fix them?

Each warning is a check Supabase runs against your project, shown under a lint name such as rls_disabled_in_public, security_definer_view, function_search_path_mutable or sensitive_columns_exposed. The ones marked ERROR, such as a public table with RLS off or a view that ignores RLS, can hand data to anyone with your project URL and come first; the ones marked WARN, such as a mutable function search path or an always-true policy, are risky patterns to fix or confirm as intended. Each section below says what the lint means and gives the fix from Supabase's lint docs.

Why this matters

The Advisor names its findings tersely: "Function Search Path Mutable" does not say whether anything is exposed, and "Security Definer View" sounds like a setting, not a leak. Supabase also says some findings may be intentional. So for each one, the useful question is what it lets a caller do, and whether you meant that.

How to work through the Security Advisor

  • Open the Security Advisor in the dashboard, or run supabase db advisors from the Supabase CLI. The Supabase MCP server (get_advisors with type set to security) and the Management API return the same checks.
  • Fix the errors first. Supabase's lint docs mark rls_disabled_in_public, security_definer_view, sensitive_columns_exposed and rls_references_user_metadata as ERROR, among others: each can hand data to callers who should not have it.
  • For each warning, decide whether it is intended. Supabase says "Some findings may be intentional, so check them against your intended schema and access model before you change anything."
  • Put every fix in a migration, not only in the SQL editor, so the next environment gets it too. Supabase's RLS guide says a migration keeps the change "reproducible across environments".
  • Run the Advisor again after the fix. Supabase notes that health findings can take a while to clear, because they measure error rates over time.

The common Security Advisor warnings, one by one

Each title is the lint name, then the name the dashboard shows. The SQL is Supabase's fix, adapted to a sample table, and was run on Postgres before publishing.

rls_disabled_in_public: RLS Disabled in Public

Level ERROR, lint 0013. A table in the public schema has row level security off. Supabase's lint docs: "Anyone with your project URL can read, edit, and delete all data in this table because Row-Level Security is not enabled."

Fix: enable RLS, then add a policy for each command the app uses. Until you add policies, the anon role cannot read or write the table through the API.

An alternative, not the fix: for a project that does not use the Data API at all, the lint docs also mention taking the schema out of the exposed schemas in the API settings. That hides everything in the schema from the API, not just this table.

alter table public.blog enable row level security;

create policy "Authors read their own posts"
on public.blog for select
to authenticated
using ( (select auth.uid()) = user_id );

security_definer_view: Security Definer View

Level ERROR, lint 0010. A view in the public schema runs with its creator's permissions, so it ignores the RLS policies of the tables underneath. Supabase's lint docs say such a view "could expose more data through the API than intended".

Fix: on Postgres 15 and later, make the view run with the caller's permissions, so the table policies apply.

An alternative, not from the lint docs: for Postgres older than 15, Supabase's RLS guide suggests revoking the view from anon and authenticated, or moving it to a schema the API does not expose.

alter view public.order_items set (security_invoker = true);

function_search_path_mutable: Function Search Path Mutable

Level WARN, lint 0011. The function has no fixed search_path, so unqualified names inside it resolve through the caller's search_path. Supabase's lint docs: "Malicious users could potentially exploit the search_path to direct the function to use unexpected objects". It matters most for security definer functions, which run with their owner's rights.

Fix: pin search_path to an empty string and schema-qualify every name in the body. If the body is already qualified, alter function public.my_function() set search_path = ''; is enough. If it is not, the function fails afterwards with relation "profiles" does not exist, so rewrite it like this:

create or replace function public.handle_new_user()
returns trigger
language plpgsql
security definer
set search_path = ''
as $$
begin
  insert into public.profiles (id) values (new.id);
  return new;
end;
$$;

rls_policy_always_true: RLS Policy Always True (lint 0024)

Level WARN. Supabase's lint docs list it as 0024_permissive_rls_policy. A policy whose condition is always true, such as using (true) or with check (true), means "these permissive policies allow unrestricted access to all rows for the specified roles", even though RLS is on.

Supabase's docs name public read-only tables such as blog posts or product catalogs as cases where it can be intentional. The docs also list using (true) on select among the patterns, while the lint's source skips select policies; the October 30 guide below covers that, and we did not run the hosted Advisor to settle it, so check your read policies yourself.

Fix: replace the condition with one that checks the caller:

drop policy "allow_all_select" on public.user_data;

create policy "users_own_data"
on public.user_data for select
to authenticated
using ( (select auth.uid()) = user_id );

sensitive_columns_exposed: Sensitive Columns Exposed

Level ERROR, lint 0023. A table the Data API can reach has RLS off and a column whose name matches a sensitive pattern, such as password, token, api_key, ssn or credit_card. In Supabase's example, "Any user with the project URL can query this table and retrieve all user passwords and social security numbers."

Fix: enable RLS with a policy, move the column to a separate table that has RLS, or drop it if you do not need it. Passwords do not belong in your own tables at all: Supabase Auth stores bcrypt hashes of user passwords in auth.users.

alter table public.users enable row level security;

create policy "Users can view own profile"
on public.users for select
to authenticated
using ( (select auth.uid()) = id );

rls_references_user_metadata: RLS references user metadata

Level ERROR, lint 0015. A policy reads user_metadata from the JWT, for example an is_admin flag. Supabase's lint docs say users can change user_metadata themselves, for example with updateUser({ data: { is_admin: true } }) in supabase-js, so the policy can be bypassed. It is the metadata set at sign-up, which is what makes it look like a natural home for a role.

Fix: the lint docs say there is no one-size-fits-all replacement. Supabase's RLS guide says app_metadata cannot be updated by the user, so it is the place for authorization data: set it from your server with auth.admin.updateUserById() and read it in the policy instead. A JWT carries the value from when it was issued, so a change takes effect when the user's token refreshes.

drop policy bad_policy on public.foo;

create policy "Admins read foo"
on public.foo for select
to authenticated
using ( coalesce(((select auth.jwt()) -> 'app_metadata' ->> 'is_admin')::bool, false) );

auth_leaked_password_protection: Leaked Password Protection Disabled

An Auth setting, not a database lint, so Supabase's lint docs do not list it; the Advisor links it to the password security docs. Supabase Auth can reject passwords known from leaks by checking them against the HaveIBeenPwned Pwned Passwords API. With it off, users can sign up with passwords attackers already have in their lists.

Fix: turn on leaked password protection in the Auth settings for the Email provider, where you also set a minimum length and required characters. Supabase says it is available on the Pro Plan and above, so on the Free Plan you cannot turn it on.

anon_security_definer_function_executable: Public Can Execute SECURITY DEFINER Function

Level WARN, lint 0028. A security definer function "runs with the privileges of its owner rather than the caller", and if anon can execute it, anyone with the public key can call it through /rest/v1/rpc. Supabase says to address lint 0029, the same finding for signed-in users, together with it, because revoking from anon alone usually leaves the function callable by every signed-up user. The RPC functions guide below covers it in full; the short fix, for a function only your server calls:

revoke execute on function public.all_notes() from public, anon, authenticated;

Optional repo check

For AI-built Vercel + Supabase apps, the free Supabase RLS checker (linked below) and npx -y viberaven@1.6.3 check catch some of these in migration SQL before they reach the database: a public table without RLS (what rls_disabled_in_public reports), using (true) policies (lint 0024) and security definer functions the anon key can call. In runs on 2026-10-04 and 2026-10-05 the check did not flag a view without security_invoker, a function without a fixed search_path or a policy that reads user_metadata (lint 0015), and neither tool reads Auth settings such as leaked password protection. They read files, while the Advisor reads your project, so use both. It is advice, not a gate, and a repository check, not a live database or security test. It exits with code 1 when it finds a blocker and writes a .viberaven folder that it does not add to .gitignore.

Related guides

Sources

Can I ignore a Security Advisor warning?

Yes, when it describes something you meant. Supabase says some findings may be intentional, and its lint docs name public read-only tables such as blog posts or product catalogs as a case where using (true) is fine. Write down why, so the next person does not undo it.

How do I run the Security Advisor from the CLI?

supabase db advisors runs the same checks from the Supabase CLI. The Supabase MCP server exposes them as get_advisors with type set to security, and the Management API has a security advisors endpoint.