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:
- Add an
escapePostgrestValue(term) helper that percent-encodes or strips the grammar characters , ( ) . * " \ and add unit tests for each of them.
- Rewrite
fetchSavedProspectsPage, fetchSavedProspectsExportRows, and deleteSavedProspect on top of the supabase-js client rather than raw fetch and hand-built URLs.
- 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.
Problem
src/app/(site)/prospects/savedProspectsData.tsbuilds PostgREST filter expressions by string concatenation of user input: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 theor(group early, and the request is either rejected with a 400 or parsed as a different filter than intended.a.bis read as a column-and-operator separator.*is theilikewildcard, 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-jsis already a dependency and is already used insrc/utils/supabaseClient.ts. Its query builder escapes filter values for you:Note that even with the client you must escape the value for
or(), so:escapePostgrestValue(term)helper that percent-encodes or strips the grammar characters, ( ) . * " \and add unit tests for each of them.fetchSavedProspectsPage,fetchSavedProspectsExportRows, anddeleteSavedProspecton top of the supabase-js client rather than rawfetchand hand-built URLs.content-rangecount handling still works (supabase-js exposescount).Done when
Foo (Bar),a.b,50%, andx*yreturns sensible results instead of errors or wrong rows.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.