Dev.to Security 🔐 Cybersecurity 👁 0 📖 2 min read

The Supabase view Claude created that anyone could read

Claude Code created a view for me through an MCP SQL tool: a clean create view public.my_view as ..., no security_invoker. A check found the problem the next day. The view was readable with the anon key, the one that sh

Claude Code created a view for me through an MCP SQL tool: a clean create view public.my_view as ..., no security_invoker.

A check found the problem the next day. The view was readable with the anon key, the one that ships in the frontend.

Why it happens

The tables had row level security. The view didn't care. In Postgres, a view runs with the rights of the user who created it, not the user who queries it. The creator was postgres, which bypasses RLS. And Supabase has long granted select on new objects in public to anon and authenticated by default. Its changelog announces a change for new tables from October 30, so check your project rather than trust either rule.

So a view in public without security_invoker is a hole straight through your policies. Claude knows this if you ask. It doesn't think about it while it's focused on making the dashboard work.

The fix is one option:

create view public.my_view
with (security_invoker = true) as
select ...;

The same trap exists with security definer functions in public (callable by anon unless you revoke execute), and with create table without enable row level security.

Catch it before it runs

If Claude talks to your database through an MCP tool, a PreToolUse hook sees every SQL statement before it's sent. A minimal version for the view case:

#!/usr/bin/env python3
import json, re, sys

event = json.load(sys.stdin)
sql = json.dumps(event.get("tool_input") or {})
for stmt in re.findall(r"create\s+(?:or\s+replace\s+)?view\s+[^;]+", sql, re.I):
    if re.search(r"\bpublic\.", stmt, re.I) and "security_invoker" not in stmt.lower():
        print("This view in public runs with its creator's rights and anon can read it. "
              "Add with (security_invoker = true).", file=sys.stderr)
        sys.exit(2)

Register it with "matcher": "mcp__.*" so it runs on MCP tool calls. Claude gets the reason and rewrites the statement with security_invoker.

What I use now

The snippet above closes the common case. A fuller version, which also catches views with no schema, quoted names and materialized views, is free in seatbelt-lite.

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.