Skip to content
ShipSecurelySecurity reference
Menu
Your tool here — sponsor ShipSecurely

Issues

SQL injection

User input glued into a SQL string can change the query. The database then runs the attacker's statement with your connection's privileges.

Share link · Share on X

Last updated

highInjection

Shows up in: Supabase, Next.js, Postgres

Markdown version

What it is

SQL injection happens when the text of a query is built from a string the caller controls. The database cannot tell your SQL from theirs.

The Supabase JavaScript client parameterizes ordinary filters such as .eq("id", value). That path is not the usual hole. The holes are raw SQL in an RPC, string-built filters passed to .or() or .filter(), and a server that uses postgres or Prisma's $queryRawUnsafe with concatenation.

A secret key or a direct Postgres connection makes the impact worse, because that connection can bypass row level security. Injection into a narrowly privileged role is still serious. Injection into a bypass role is the whole database.

Firestore is not SQL. The cousin bug there is trusting client-built queries and open rules, not a classic OR 1=1 string.

How to check in 5 minutes

  1. Search the repo for sql\``, $queryRawUnsafe, .query(, execute(, .or(, and rpc(`. Open each hit and see if a request value is concatenated into the string.

  2. Search migrations and database functions for execute that interpolates text. A Postgres function that builds dynamic SQL from a parameter is a sink even if the TypeScript looks clean.

  3. On a local or staging project you own, send a value like a' or '1'='1 through a search box that hits one of those sinks. An error that quotes your input back, or a result set that ignores the filter, is a sign the string was parsed as SQL. Stop there. Do not try to dump data.

  4. Confirm production queries use parameters. If you cannot explain why a string is safe, treat it as unsafe.

How to fix

Supabase

Keep filters in the client methods that take a value separately from the column name. Do not pass user text as a fragment of an .or() filter string. If you need a search, use a parameterized RPC:

create function search_notes(q text)
returns setof public.notes
language sql
stable
as $$
  select * from public.notes
  where user_id = auth.uid()
    and body ilike '%' || replace(replace(q, '%', ''), '_', '') || '%';
$$;

Still enable RLS, or define the function so it does not run as a privileged owner. Supabase documents that security-definer functions and views can bypass RLS. Prefer security invoker unless you have a reason and a review.

Never concatenate q into execute format('... %s ...', q) without a format that binds it as a value (%L) and a fixed statement shape.

Next.js

With Prisma, use the tagged template prisma.$queryRaw, which parameterizes interpolations. Do not use $queryRawUnsafe with a built string. With postgres or pg, pass values as parameters ($1, $2), not as part of the query text.

Do not let the caller choose the column name unless you check it against a list you wrote.

Firebase

You do not fix this with a SQL driver because Firestore queries are structured. You still must not let the client decide which collection to read. Rules do that. See Firebase rules left open.

Vercel

The host runs your function. It does not parameterize your SQL. The fix is in the query.

Copy-paste prompt

Paste this into Cursor, Claude Code, or whichever agent is editing the repo. Read the diff before you merge it.

You are removing SQL injection. Do not add features.

Search for raw SQL, $queryRawUnsafe, string-built .or() or .filter() calls, and database functions that concatenate SQL.

For each hit:
1. If user input is concatenated into the query text, switch to bound parameters.
2. If a caller can choose a column or table name, allow only names from a fixed list in code.
3. Do not "sanitize" by stripping quotes. Parameterize instead.
4. Leave RLS enabled. Do not mark a function security definer to make the query easier.

Show the before and after for each query. If you are unsure a query is reachable from a request, say so instead of changing it.

Questions

Our Supabase calls use .eq(). Are we safe?

Those values are parameters. You are not safe if another path builds SQL or filter strings by hand, or if a Postgres function does.

Is an ORM enough?

ORMs parameterize the queries they generate. They do not parameterize a raw string you hand them. Read the raw calls.

Sources

Related