# Do not pass raw query strings into PostgREST .or()

> Supabase .or() accepts PostgREST filter grammar, not a harmless search string. Build filters from allowed fields, operators, and typed values instead.

Source: https://saascode.ai/inside/postgrest-or-filter-injection · Published: 2026-08-21 · Section: academy

---
A flexible filter endpoint can become a small query language without anyone meaning to build one.

The risky line looks convenient:

```ts
const filter = new URL(request.url).searchParams.get('filter')
const { data, error } = await supabase.from('customers').or(filter)
```

PostgREST's `or` parameter accepts expressions such as `age.lt.18` and nested logical conditions. If the caller controls the whole string, the caller also chooses the columns, operators, and shape of the condition. That is filter injection.

It is worth being precise about the impact. A crafted filter does not defeat a correct PostgreSQL Row Level Security policy: RLS is still applied to the query. It can, however, escape the narrower filter your application intended and return any row the database role is already allowed to see. If the server uses a service credential that bypasses RLS, that distinction can turn into a full data leak.

## Design a filter interface, not a grammar tunnel

Expose the few choices the UI actually needs:

```ts
const allowedStatuses = new Set(['active', 'paused', 'closed'])
const status = searchParams.get('status')

if (status && !allowedStatuses.has(status)) {
  return Response.json({ error: 'Invalid status' }, { status: 400 })
}

let query = supabase.from('customers').select('id,name,status')

if (status) query = query.eq('status', status)
```

For sortable lists, map public sort keys to known database columns. For numeric and date filters, parse and bound the value before building the query. For free-text search across several columns, construct the expression server-side from a single escaped value, or move the logic into a database function with a typed argument.

An allowlist must cover three things:

- fields the caller may filter;
- operators permitted for each field;
- value parsing, length, and encoding.

Checking only the field name still leaves an attacker in control of the operator or expression value. Concatenating a validated field with an unescaped value still leaves a grammar boundary open.

## Audit the raw-expression methods

Search for `.or(` first, because its argument is commonly hand-built. Then review any helper that accepts raw PostgREST syntax. Trace each argument back to its source. A constant or a fully server-constructed expression is one thing; a query parameter passed through unchanged is another.

One production route exposed this exact shape on an administrative customer list. The useful lesson is broader than that route: do not let an HTTP parameter become a second query language inside your API. Give callers a small, documented filter contract and keep the PostgREST grammar on the server.
