Skip to content

Conversation

@jonathan-fulton
Copy link

Summary

This PR adds new API endpoints for simulating and debugging Row Level Security (RLS) policies. RLS debugging is consistently one of the most painful aspects of working with Supabase, and this feature provides a way to test policies without modifying data.

Problem

Debugging RLS policies is currently very difficult:

  • You have to guess why rows aren't appearing
  • Testing different user contexts requires actual authentication
  • There's no way to see which specific policy is blocking access
  • Policy expressions are opaque - you can't see intermediate results

Solution

New API endpoints under /rls-playground/:

Endpoint Method Description
/roles GET List available database roles for simulation
/tables?schema=X GET List tables with RLS status and policy counts
/policies/:schema/:table GET Get policies for a specific table
/rls-status/:schema/:table GET Check if RLS is enabled
/simulate POST Run policy simulation with custom context
/evaluate-expression POST Test a single policy expression

Simulation Features

The /simulate endpoint allows:

  • Setting a database role (anon, authenticated, service_role, or custom)
  • Providing JWT claims to simulate auth.jwt()
  • Seeing which rows are accessible under the simulated context
  • Getting policy-by-policy evaluation results

Safety

All simulations run in rolled-back transactions - no data is ever modified.

Technical Details

  • New PostgresMetaRLSPlayground class in src/lib/
  • Uses PostgreSQL set_config() for context simulation
  • Evaluates policy expressions per-row for detailed debugging
  • SQL injection prevention via pg-format literals

Testing

Manual testing with various RLS configurations. Automated tests can be added in a follow-up.

Related

This PR provides the backend for the Studio RLS Playground UI (separate PR to supabase/supabase).

Adds new API endpoints for simulating and debugging Row Level Security policies:

- GET /rls-playground/roles - List available database roles
- GET /rls-playground/tables?schema=X - List tables with RLS status
- GET /rls-playground/policies/:schema/:table - Get policies for a table
- GET /rls-playground/rls-status/:schema/:table - Check if RLS is enabled
- POST /rls-playground/simulate - Run policy simulation with custom context
- POST /rls-playground/evaluate-expression - Test single policy expression

The simulation endpoint allows users to:
- Set a database role (anon, authenticated, service_role, custom)
- Provide JWT claims to simulate auth.jwt()
- See which rows are accessible under the simulated context
- Get policy-by-policy evaluation results for debugging

All simulations run in rolled-back transactions for safety.
@snyk-io
Copy link

snyk-io bot commented Jan 31, 2026

Snyk checks have failed. 3 issues have been found so far.

Status Scanner Critical High Medium Low Total (3)
Code Security 0 3 0 0 3 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

const pgMeta = new PostgresMeta(config)
const rlsPlayground = new PostgresMetaRLSPlayground(pgMeta.query)

const { data, error } = await rlsPlayground.simulate({
Copy link

Choose a reason for hiding this comment

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

  SQL Injection

Unsanitized input from the HTTP request body flows into query, where it is used in an SQL query. This may result in an SQL Injection vulnerability.

Line 123 | CWE-89 | Priority score 900 | Learn more about this vulnerability
Data flow: 18 steps

Step 1 - 4

const { schema, table, operation, context, limit, testData } = request.body

Step 5 - 6 src/server/routes/rls-playground.ts#L124

Step 7 src/server/routes/rls-playground.ts#L123

Step 8 src/lib/PostgresMetaRLSPlayground.ts#L106

Step 9 src/lib/PostgresMetaRLSPlayground.ts#L108

Step 10 - 13 src/lib/PostgresMetaRLSPlayground.ts#L271

Step 14 src/lib/PostgresMetaRLSPlayground.ts#L308

Step 15 src/lib/PostgresMetaRLSPlayground.ts#L289

Step 16 src/lib/PostgresMetaRLSPlayground.ts#L141

Step 17 - 18

const { data: simResult, error: simError } = await this.query(simulationSql)

const pgMeta = new PostgresMeta(config)
const rlsPlayground = new PostgresMetaRLSPlayground(pgMeta.query)

const { data, error } = await rlsPlayground.evaluateExpression({
Copy link

Choose a reason for hiding this comment

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

  SQL Injection

Unsanitized input from the HTTP request body flows into query, where it is used in an SQL query. This may result in an SQL Injection vulnerability.

Line 167 | CWE-89 | Priority score 900 | Learn more about this vulnerability
Data flow: 21 steps

Step 1 - 4

const { schema, table, expression, context, testRow } = request.body

Step 5 - 6 src/server/routes/rls-playground.ts#L168

Step 7 src/server/routes/rls-playground.ts#L167

Step 8 src/lib/PostgresMetaRLSPlayground.ts#L403

Step 9 src/lib/PostgresMetaRLSPlayground.ts#L405

Step 10 - 13 src/lib/PostgresMetaRLSPlayground.ts#L416

Step 14 - 15 src/lib/PostgresMetaRLSPlayground.ts#L424

Step 16 src/lib/PostgresMetaRLSPlayground.ts#L420

Step 17 src/lib/PostgresMetaRLSPlayground.ts#L436

Step 18 - 19 src/lib/PostgresMetaRLSPlayground.ts#L426

Step 20 - 21

const { data, error } = await this.query(sql)

ORDER BY c.relname
`

const { data, error } = await pgMeta.query(sql)
Copy link

Choose a reason for hiding this comment

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

  SQL Injection

Unsanitized input from an HTTP parameter flows into query, where it is used in an SQL query. This may result in an SQL Injection vulnerability.

Line 222 | CWE-89 | Priority score 900 | Learn more about this vulnerability
Data flow: 13 steps

Step 1 - 7

const schema = request.query.schema || 'public'

Step 8 - 9 src/server/routes/rls-playground.ts#L217

Step 10 - 11 src/server/routes/rls-playground.ts#L202

Step 12 - 13

const { data, error } = await pgMeta.query(sql)

Security fixes for Snyk HIGH severity issues:

1. Added validateRLSExpression() function to reject dangerous SQL patterns:
   - DDL statements (CREATE, DROP, ALTER, TRUNCATE)
   - DML statements (INSERT, UPDATE, DELETE)
   - Transaction control (COMMIT, ROLLBACK, SAVEPOINT)
   - Security commands (GRANT, REVOKE, SET ROLE)
   - Dangerous functions (pg_read_file, pg_terminate_backend, etc.)
   - Comment injection (-- and /*)
   - Statement separators (;)

2. evaluateExpression: Now validates user-provided expressions before execution
   and runs in a READ ONLY transaction that always rolls back

3. buildSelectSimulation: Added expression validation for policy definitions
   (defense in depth) and proper transaction isolation with ROLLBACK

4. buildInsertSimulation: Added validation for policy.check expressions
   and uses literal() for policy.id to prevent any potential injection

5. Route validation: Added input length limits and type validation
   for expression and context.role parameters

6. Limit sanitization: Ensured limit parameter is always a safe integer
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant