Skip to content

Prospect search interpolates raw user input into a PostgREST or() filter #50

Description

@royalpinto007

Problem

src/app/(site)/prospects/savedProspectsData.ts builds PostgREST filter expressions by string concatenation of user input:

function applyFilters(url: URL, query: SavedProspectsQuery) {
  if (query.search.trim()) {
    const term = query.search.trim().replace(/,/g, " ");
    url.searchParams.set(
      "or",
      `(title.ilike.*${term}*,domain.ilike.*${term}*,url.ilike.*${term}*)`,
    );
  }
  ...
}

The only sanitization is stripping commas. PostgREST's filter grammar also treats (, ), ., *, and " as structure. A search term containing any of those does not search for that character, it rewrites the expression. Examples worth trying against a dev project:

  • a) closes the or( group early, and the request is either rejected with a 400 or parsed as a different filter than intended.
  • a.b is read as a column-and-operator separator.
  • * is the ilike wildcard, so it silently changes the match semantics.

At minimum this is a broken feature: a user searching for a company named Foo (Bar) gets an error or wrong results. At worst it is filter injection against a table the anon key can reach, since the query goes directly from the browser to /rest/v1/saved_analyses.

Why it matters

This is the same class of problem as SQL injection, one layer up. Row Level Security is presumably what stops it from becoming a data leak, but the safety of the feature should not rest entirely on RLS being configured perfectly, and the correctness bug is user-visible today regardless.

Suggested approach

The clean fix is to stop hand-rolling PostgREST. @supabase/supabase-js is already a dependency and is already used in src/utils/supabaseClient.ts. Its query builder escapes filter values for you:

supabase.from("saved_analyses").select(...).or(`title.ilike.%${escaped}%,...`)

Note that even with the client you must escape the value for or(), so:

  1. Add an escapePostgrestValue(term) helper that percent-encodes or strips the grammar characters , ( ) . * " \ and add unit tests for each of them.
  2. Rewrite fetchSavedProspectsPage, fetchSavedProspectsExportRows, and deleteSavedProspect on top of the supabase-js client rather than raw fetch and hand-built URLs.
  3. Confirm the pagination content-range count handling still works (supabase-js exposes count).

Done when

  • Searching for Foo (Bar), a.b, 50%, and x*y returns sensible results instead of errors or wrong rows.
  • Filter values are escaped by one tested helper, not by an inline replace.

If you want to take this on, comment on the issue to claim it and it will be assigned. Please keep to a maximum of 2 open claims per person at a time so other contributors get a chance.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingenhancementNew feature or requesthelp wantedExtra attention is needed

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions