SQL Injection in Supabase Applications
Supabase's JavaScript client library parameterizes queries automatically via PostgREST. However, custom RPC functions with dynamic SQL, database triggers using EXECUTE with string concatenation, and raw SQL in migrations or Edge Functions can introduce SQL injection vulnerabilities.
Scan Your Supabase AppHow SQL Injection Manifests in Supabase
The Supabase client (supabase.from().select()) is safe because PostgREST parameterizes all queries. SQL injection occurs in: - RPC functions (supabase.rpc()) where the underlying PostgreSQL function uses EXECUTE with string concatenation - Database triggers and functions that build dynamic SQL without parameterization - Edge Functions that connect directly to PostgreSQL and build raw queries - Migrations that create functions with SQL string concatenation Developers sometimes create PostgreSQL functions to bypass RLS for admin operations, and these functions often use unsafe dynamic SQL.
Real-World Impact
A Supabase application created an RPC function for full-text search that used EXECUTE with string concatenation to build a dynamic LIKE query. An attacker called the RPC function with a SQL injection payload that bypassed RLS policies and extracted data from tables the user had no access to, including private messages and payment records.
Step-by-Step Fix
Use parameterized dynamic SQL in functions
Use EXECUTE ... USING instead of string concatenation in PostgreSQL functions.
-- UNSAFE function
CREATE OR REPLACE FUNCTION search_products(search_term text)
RETURNS SETOF products AS $$
BEGIN
RETURN QUERY EXECUTE
'SELECT * FROM products WHERE name LIKE ''%' || search_term || '%''';
END;
$$ LANGUAGE plpgsql;
-- SAFE function
CREATE OR REPLACE FUNCTION search_products(search_term text)
RETURNS SETOF products AS $$
BEGIN
RETURN QUERY EXECUTE
'SELECT * FROM products WHERE name LIKE $1'
USING '%' || search_term || '%';
END;
$$ LANGUAGE plpgsql;Use format() for dynamic identifiers
When table or column names must be dynamic, use format() with %I.
-- UNSAFE - dynamic table name
CREATE OR REPLACE FUNCTION get_records(table_name text)
RETURNS SETOF record AS $$
BEGIN
RETURN QUERY EXECUTE 'SELECT * FROM ' || table_name;
END;
$$ LANGUAGE plpgsql;
-- SAFE - format with %I for identifiers
CREATE OR REPLACE FUNCTION get_records(table_name text)
RETURNS SETOF record AS $$
BEGIN
-- Validate table name against allowlist
IF table_name NOT IN ('products', 'categories', 'reviews') THEN
RAISE EXCEPTION 'Invalid table name';
END IF;
RETURN QUERY EXECUTE format('SELECT * FROM %I', table_name);
END;
$$ LANGUAGE plpgsql;Secure Edge Functions
Use parameterized queries in Supabase Edge Functions.
// UNSAFE Edge Function
import { Pool } from 'https://deno.land/x/postgres/mod.ts';
const pool = new Pool(Deno.env.get('DATABASE_URL')!, 3);
Deno.serve(async (req) => {
const { search } = await req.json();
const conn = await pool.connect();
// UNSAFE
const result = await conn.queryObject(
`SELECT * FROM users WHERE name = '${search}'`
);
// SAFE
const result = await conn.queryObject(
'SELECT * FROM users WHERE name = $1',
[search]
);
conn.release();
return new Response(JSON.stringify(result.rows));
});Prevention Best Practices
1. Use the Supabase client library for all standard CRUD operations. 2. In custom RPC functions, use EXECUTE ... USING for parameterization. 3. Never use || (string concatenation) with user input in PL/pgSQL functions. 4. Apply SECURITY DEFINER only when absolutely necessary, with strict input validation. 5. Use format() with %I for identifiers and %L for literals in dynamic SQL.
How to Test
1. Review all custom PostgreSQL functions in your migrations for string concatenation. 2. Search for EXECUTE with || in function bodies. 3. Test RPC endpoints by passing SQL payloads as function arguments. 4. Check Edge Functions for raw SQL with template literals. 5. Use Vibe App Scanner to detect SQL injection in your Supabase application.
Frequently Asked Questions
Is the Supabase client library safe from SQL injection?
Yes. The Supabase JavaScript client communicates with PostgREST, which parameterizes all queries automatically. supabase.from("table").select() and similar methods are safe. SQL injection only occurs in custom RPC functions, database triggers, and Edge Functions that build raw SQL.
Can RLS prevent SQL injection in Supabase?
No. RLS controls which rows a user can access, but it does not prevent SQL injection in custom functions. A SECURITY DEFINER function runs with the function owner's permissions, potentially bypassing RLS entirely. SQL injection in such a function gives attackers full database access.
Are Supabase anon keys related to SQL injection?
No. Supabase anon keys are public API keys that authenticate with PostgREST. They do not affect SQL injection risk. SQL injection occurs at the database function level, not the API authentication level.
Related Security Resources
Is Your Supabase App Vulnerable to SQL Injection?
VAS automatically scans for sql injection vulnerabilities in Supabase applications and provides step-by-step remediation guidance with code examples.
Scans from $5, results in minutes. Get actionable fixes tailored to your Supabase stack.