Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .claude/skills/new-lint/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ Create a SQL view in the `lint` schema. The view **must** return exactly these 1
| `metadata` | jsonb | `jsonb_build_object('schema', ..., 'name', ..., 'type', ...)` |
| `cache_key` | text | `format('<name>_%s_%s', schema, object)` — unique per violation |

**Copy guidance for Advisor surfaces:**
- Keep `description` to 1-2 short sentences: what the lint detects and the first likely action.
- Keep `detail` concise and object-specific. It should describe the failing object, not restate the full rationale.
- Put deeper explanation, edge cases, and extended remediation in `docs/XXXX_<name>.md`, not in the SQL strings.

Comment on lines +35 to +39
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For future lints. Helps make the Advisor side on Studio easier to read (and lints for consistent).

**Required conventions:**
- Always prefix all catalog references: `pg_catalog.pg_class`, `pg_catalog.pg_namespace`, etc.
- Always exclude system schemas using this exact list:
Expand Down
4 changes: 2 additions & 2 deletions docs/0026_pg_graphql_anon_table_exposed.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@

**Level:** WARN

**Summary:** Tables, views, materialized views, and foreign tables readable by the `anon` role have their schema (names, columns, relationships) made visible through `pg_graphql` introspection.
**Summary:** This object is visible in your GraphQL schema to anyone using the public anon key.

**Ramification:** Anyone with your public anon key can enumerate every relation the `anon` role can SELECT — even when RLS is enabled. RLS hides rows; it does not hide the schema. Both forms of introspection (PostgREST and `pg_graphql`) are intentional behavior, but you may not realize how much of your schema is reachable without authentication: every table name, column name, type, relationship, and mutation endpoint is publicly discoverable.
**Ramification:** If `anon` can `SELECT` any column on a table, view, materialized view, or foreign table, `pg_graphql` exposes that object's name, columns, relationships, and generated mutations through `/graphql/v1` introspection. RLS does not change that because it protects rows, not schema visibility. If this object should not be discoverable before sign-in, revoke `SELECT` from `anon` or disable `pg_graphql` if you do not use GraphQL.

> **See also: lint [0027_pg_graphql_authenticated_table_exposed](0027_pg_graphql_authenticated_table_exposed.md).** In default Supabase projects `anon` and `authenticated` start with identical default-privilege grants, so revoking from `anon` alone often leaves the same introspection response served to any signed-up user. Address findings from both lints together.

Expand Down
4 changes: 2 additions & 2 deletions docs/0027_pg_graphql_authenticated_table_exposed.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@

**Level:** WARN

**Summary:** Tables, views, materialized views, and foreign tables readable by the `authenticated` role have their schema (names, columns, relationships) made visible through `pg_graphql` introspection to any signed-up user.
**Summary:** This object is visible in your GraphQL schema to signed-in users.

**Ramification:** Every relation the `authenticated` role can SELECT is enumerable by anyone with a valid Supabase user JWT — even when RLS is enabled. RLS hides rows; it does not hide the schema. In default Supabase projects the `authenticated` role starts with the same default-privilege grants as `anon` (both come from `ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin … TO anon, authenticated, service_role`), and under open or auto-confirm signup, "authenticated" is in practice anyone with a throwaway email rather than a meaningfully smaller audience.
**Ramification:** If `authenticated` can `SELECT` any column on a table, view, materialized view, or foreign table, `pg_graphql` exposes that object's name, columns, relationships, and generated mutations through `/graphql/v1` introspection to signed-in users. RLS does not change that because it protects rows, not schema visibility. In projects with open signup, that can mean any throwaway account, so revoke `SELECT` from `authenticated` for objects that every account holder should not discover.

> **See also: lint [0026_pg_graphql_anon_table_exposed](0026_pg_graphql_anon_table_exposed.md).** The two checks are paired — revoking from one role alone usually leaves the other side of the introspection response unchanged. Address findings from both lints together.

Expand Down
4 changes: 2 additions & 2 deletions docs/0028_anon_security_definer_function_executable.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@

**Level:** WARN

**Summary:** A `SECURITY DEFINER` function in a user schema is executable by the `anon` role, meaning anyone with the public anon key can invoke a privileged operation that bypasses RLS.
**Summary:** This `SECURITY DEFINER` function is callable without signing in.

**Ramification:** A `SECURITY DEFINER` function runs with the privileges of its owner — typically a role with `BYPASSRLS` or with broad table grants — not the caller. When `anon` has `EXECUTE`, any unauthenticated request can call the function via `POST /rest/v1/rpc/<name>` and read or modify rows that RLS would otherwise hide. If pg_graphql is installed and the function's return type is supported, the same function is also callable as a Query or Mutation field via `/graphql/v1`.
**Ramification:** Because this function is `SECURITY DEFINER`, it runs with the privileges of its owner rather than the caller. If `anon` has `EXECUTE`, anyone with the public anon key can call it through `POST /rest/v1/rpc/<name>` and potentially read or modify data that RLS would normally block. If that is not intentional, revoke `EXECUTE`, switch the function to `SECURITY INVOKER`, or move it out of your exposed API schema.

> **See also: lint [0029_authenticated_security_definer_function_executable](0029_authenticated_security_definer_function_executable.md).** In default Supabase projects `anon` and `authenticated` start with identical default-privilege grants (and the Postgres default for new functions is `EXECUTE` to `PUBLIC`), so revoking from `anon` alone usually leaves the same function callable by every signed-up user. Address findings from both lints together. The `pg_graphql_*` lints (0026/0027) cover the parallel risk for tables/views.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@

**Level:** WARN

**Summary:** A `SECURITY DEFINER` function in a user schema is executable by the `authenticated` role, meaning every signed-up user can invoke a privileged operation that bypasses RLS.
**Summary:** This `SECURITY DEFINER` function is callable by signed-in users.

**Ramification:** A `SECURITY DEFINER` function runs with the privileges of its owner — typically a role with `BYPASSRLS` or with broad table grants — not the caller. When `authenticated` has `EXECUTE`, any signed-up user can call the function via `POST /rest/v1/rpc/<name>` with a real user JWT and read or modify rows that RLS would otherwise hide. If pg_graphql is installed and the function's return type is supported, the same function is also callable as a Query or Mutation field via `/graphql/v1`. Under open or auto-confirm signup, "authenticated" is anyone with a throwaway email — not a meaningfully smaller audience than `anon`.
**Ramification:** Because this function is `SECURITY DEFINER`, it runs with the privileges of its owner rather than the caller. If `authenticated` has `EXECUTE`, any signed-in user can call it through `POST /rest/v1/rpc/<name>` and potentially read or modify data that RLS would normally block. In projects with open signup, that can mean any throwaway account, so revoke `EXECUTE`, switch the function to `SECURITY INVOKER`, or move it out of your exposed API schema if every account holder should not be able to call it.

> **See also: lint [0028_anon_security_definer_function_executable](0028_anon_security_definer_function_executable.md).** The two checks are paired — revoking from one role alone usually leaves the other side callable. Address findings from both lints together. The `pg_graphql_*` lints (0026/0027) cover the parallel risk for tables/views.

Expand Down
6 changes: 3 additions & 3 deletions lints/0026_pg_graphql_anon_table_exposed.sql
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,13 @@ exposed_objects as (
)
select
'pg_graphql_anon_table_exposed' as name,
'pg_graphql Anon Role Exposes Objects in Introspection' as title,
'Public Can See Object in GraphQL Schema' as title,
'WARN' as level,
'EXTERNAL' as facing,
array['SECURITY'] as categories,
'Detects tables, views, materialized views, and foreign tables whose schema is visible via the public `/graphql/v1` introspection endpoint. When `pg_graphql` is installed, any object the `anon` role has `SELECT` on is visible in introspection — names, columns, types, and relationships — even when RLS is enabled. See lint 0027 for the equivalent check against the `authenticated` role; in default Supabase projects revoking from `anon` alone is not sufficient.' as description,
'Detects tables, views, materialized views, and foreign tables that are visible in the GraphQL schema to anyone using your public anon key. Revoke `SELECT` from `anon` for objects that should not be discoverable before sign-in, and check lint 0027 for the matching signed-in-user exposure.' as description,
format(
'Extension `pg_graphql` is installed and the `anon` role has `SELECT` on %s `%s.%s`. Its name, columns, and relationships are visible via the public `/graphql/v1` introspection endpoint.',
'%s `%s.%s` is visible in the GraphQL schema because the `anon` role can `SELECT` it. Revoke `SELECT` from `anon` if it should not be discoverable without signing in.',
object_type,
schema_name,
object_name
Expand Down
6 changes: 3 additions & 3 deletions lints/0027_pg_graphql_authenticated_table_exposed.sql
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,13 @@ exposed_objects as (
)
select
'pg_graphql_authenticated_table_exposed' as name,
'pg_graphql Authenticated Role Exposes Objects in Introspection' as title,
'Signed-In Users Can See Object in GraphQL Schema' as title,
'WARN' as level,
'EXTERNAL' as facing,
array['SECURITY'] as categories,
'Detects tables, views, materialized views, and foreign tables whose schema is visible via the `/graphql/v1` introspection endpoint to any signed-up user. When `pg_graphql` is installed, any object the `authenticated` role has `SELECT` on is visible in introspection — names, columns, types, and relationships — even when RLS is enabled. In default Supabase projects `authenticated` is anyone with a valid JWT, which under open or auto-confirm signup is anyone with a throwaway email. See lint 0026 for the equivalent check against `anon`.' as description,
'Detects tables, views, materialized views, and foreign tables that are visible in the GraphQL schema to signed-in users. Revoke `SELECT` from `authenticated` for objects that signed-in users should not discover, and check lint 0026 for the matching public exposure.' as description,
format(
'Extension `pg_graphql` is installed and the `authenticated` role has `SELECT` on %s `%s.%s`. Its name, columns, and relationships are visible via the `/graphql/v1` introspection endpoint to any signed-up user.',
'%s `%s.%s` is visible in the GraphQL schema to signed-in users because the `authenticated` role can `SELECT` it. Revoke `SELECT` from `authenticated` if it should not be discoverable to every account.',
object_type,
schema_name,
object_name
Expand Down
6 changes: 3 additions & 3 deletions lints/0028_anon_security_definer_function_executable.sql
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ create view lint."0028_anon_security_definer_function_executable" as
-- commonly deployed surface for the same risk.
select
'anon_security_definer_function_executable' as name,
'SECURITY DEFINER Function Executable by Anon' as title,
'Public Can Execute SECURITY DEFINER Function' as title,
'WARN' as level,
'EXTERNAL' as facing,
array['SECURITY'] as categories,
'Detects SECURITY DEFINER functions that the `anon` role has EXECUTE on. A SECURITY DEFINER function runs with the privileges of its owner and bypasses RLS, so granting EXECUTE to `anon` lets any holder of the public anon key invoke a privileged operation via PostgREST `/rest/v1/rpc/<name>` (and via `/graphql/v1` when pg_graphql is installed and the function''s return type is supported). See lint 0029 for the equivalent check against the `authenticated` role.' as description,
'Detects `SECURITY DEFINER` functions that are callable without signing in. Revoke `EXECUTE`, switch the function to `SECURITY INVOKER`, or move it out of your exposed API schema if it is not meant to be public.' as description,
format(
'SECURITY DEFINER function `%s.%s(%s)` is executable by the `anon` role. It runs with the privileges of its owner and bypasses RLS, so any unauthenticated caller can invoke it via `/rest/v1/rpc/%s`.',
'Function `%s.%s(%s)` can be executed by the `anon` role as a `SECURITY DEFINER` function via `/rest/v1/rpc/%s`. Revoke `EXECUTE` or switch it to `SECURITY INVOKER` if that is not intentional.',
schema_name,
function_name,
function_args,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ create view lint."0029_authenticated_security_definer_function_executable" as
-- commonly deployed surface for the same risk.
select
'authenticated_security_definer_function_executable' as name,
'SECURITY DEFINER Function Executable by Authenticated' as title,
'Signed-In Users Can Execute SECURITY DEFINER Function' as title,
'WARN' as level,
'EXTERNAL' as facing,
array['SECURITY'] as categories,
'Detects SECURITY DEFINER functions that the `authenticated` role has EXECUTE on. A SECURITY DEFINER function runs with the privileges of its owner and bypasses RLS, so granting EXECUTE to `authenticated` lets any signed-up user invoke a privileged operation via PostgREST `/rest/v1/rpc/<name>` (and via `/graphql/v1` when pg_graphql is installed and the function''s return type is supported). Under open or auto-confirm signup `authenticated` is anyone with a throwaway email. See lint 0028 for the equivalent check against the `anon` role.' as description,
'Detects `SECURITY DEFINER` functions that are callable by signed-in users. Revoke `EXECUTE`, switch the function to `SECURITY INVOKER`, or move it out of your exposed API schema if signed-in users should not call it.' as description,
format(
'SECURITY DEFINER function `%s.%s(%s)` is executable by the `authenticated` role. It runs with the privileges of its owner and bypasses RLS, so any signed-up user can invoke it via `/rest/v1/rpc/%s`.',
'Function `%s.%s(%s)` can be executed by the `authenticated` role as a `SECURITY DEFINER` function via `/rest/v1/rpc/%s`. Revoke `EXECUTE` or switch it to `SECURITY INVOKER` if that is not intentional.',
schema_name,
function_name,
function_args,
Expand Down
5 changes: 5 additions & 0 deletions mkdocs.yaml
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adds these new lints to the sidebar/nav.

Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ nav:
- Extension Versions Outdated: '0022_extension_versions_outdated.md'
- Sensitive Columns Exposed: '0023_sensitive_columns_exposed.md'
- Permissive RLS Policy: '0024_permissive_rls_policy.md'
- Public Bucket Allows Listing: '0025_public_bucket_allows_listing.md'
- Public Can See Object in GraphQL Schema: '0026_pg_graphql_anon_table_exposed.md'
- Signed-In Users Can See Object in GraphQL Schema: '0027_pg_graphql_authenticated_table_exposed.md'
- Public Can Execute SECURITY DEFINER Function: '0028_anon_security_definer_function_executable.md'
- Signed-In Users Can Execute SECURITY DEFINER Function: '0029_authenticated_security_definer_function_executable.md'

theme:
name: 'material'
Expand Down
Loading
Loading