---
title: "SQL injection"
slug: sql-injection
severity: high
category: Injection
summary: "User input glued into a SQL string can change the query. The database then runs the attacker's statement with your connection's privileges."
platforms:
  - Supabase
  - Next.js
  - Postgres
updated: "2026-10-13"
related:
  - supabase-tables-without-rls
  - idor
---

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:

```sql
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](/issues/firebase-rules-left-open).

### Vercel

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

## Agent prompt

```text
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.
```

## FAQ

### 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

- [OWASP SQL Injection Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html)
- [Supabase database linter, including security-definer views](https://supabase.com/docs/guides/database/database-linter)
