Skip to content

Escape candidates search filters#407

Open
jsdavid278-cyber wants to merge 1 commit into
profullstack:masterfrom
jsdavid278-cyber:codex/candidates-search-escape
Open

Escape candidates search filters#407
jsdavid278-cyber wants to merge 1 commit into
profullstack:masterfrom
jsdavid278-cyber:codex/candidates-search-escape

Conversation

@jsdavid278-cyber
Copy link
Copy Markdown
Contributor

Fixes #406.

Changes:

  • escape candidate text search before composing PostgREST .or(...) filters
  • escape candidate tag filters before placing user text in quoted array literals
  • add regression tests for punctuation, LIKE wildcards, backslash, quote, and brace cases

Validation:

  • vitest run src/lib/queries/candidates.test.ts
  • vitest run src/lib/security/sanitize.test.ts src/lib/queries/candidates.test.ts
  • tsc --noEmit

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Jun 5, 2026

Greptile Summary

This PR escapes user-controlled text before it is interpolated into PostgREST .or() filter strings and PostgreSQL array literal values in buildCandidatesQuery, closing filter-injection vectors. It introduces escapePostgrestArrayLiteralValue in sanitize.ts and adds four regression tests.

  • The fix in candidates.ts is correct; agents.ts is a structural twin with the identical unescaped interpolation pattern and was not updated, leaving the agents query path exposed.
  • escapePostgrestArrayLiteralValue delegates to escapePostgrestSearchValue, which escapes LIKE wildcards (%, *, _) that have no special meaning inside a PostgreSQL double-quoted array element used with the cs operator, causing tag searches to silently fail for tags containing those characters.

Confidence Score: 3/5

Not safe to merge as-is: the agents query builder has the same unescaped filter interpolation that this PR set out to fix, and the new array-literal escape function over-escapes LIKE wildcard characters that are not meaningful in the contains-operator context.

The candidates path is correctly patched and well tested, but agents.ts — a near-identical file — still interpolates raw q and tag values directly into PostgREST filter strings. Additionally, escapePostgrestArrayLiteralValue inherits LIKE-wildcard escaping that is inappropriate for array-literal values, which would silently produce wrong results for any tag containing %, _, or *.

src/lib/queries/agents.ts needs the same escaping treatment applied to candidates.ts; src/lib/security/sanitize.ts needs the array-literal escape function to be decoupled from LIKE-wildcard escaping.

Important Files Changed

Filename Overview
src/lib/security/sanitize.ts Adds escapePostgrestArrayLiteralValue; incorrectly inherits LIKE-wildcard escaping (%, _, *) that is inappropriate for the cs contains-operator context, and the new function lacks direct unit tests in sanitize.test.ts.
src/lib/queries/candidates.ts Correctly applies escapePostgrestSearchValue to q and escapePostgrestArrayLiteralValue to each tag before building the PostgREST filter string; logic is sound for typical inputs.
src/lib/queries/candidates.test.ts Adds four well-chosen regression tests covering PostgREST filter syntax, LIKE wildcards, quotes, commas, and braces in both search and tag filter paths.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[User input: q or tags] --> B{Filter type?}
    B -- text search --> C[escapePostgrestSearchValue\nescapes backslash pct star underscore comma dot parens]
    B -- tag filter --> D[escapePostgrestArrayLiteralValue\ncalls escapePostgrestSearchValue then escapes quotes and braces]
    C --> E["query.or('full_name.ilike.%escaped%,...')"]
    D --> F["query.or('skills.cs.{escaped},...')"]
    E --> G[PostgREST API]
    F --> G
    G --> H[PostgreSQL]
    D --> W["WARNING: pct star underscore also escaped\nnot needed for cs contains operator"]
Loading

Reviews (1): Last reviewed commit: "Escape candidates search filters" | Re-trigger Greptile

Comment on lines +61 to 63
export function escapePostgrestArrayLiteralValue(value: string): string {
return escapePostgrestSearchValue(value).replace(/["{}]/g, (char) => `\\${char}`);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 LIKE-wildcard escaping incorrectly applied to array-literal values

escapePostgrestArrayLiteralValue delegates first to escapePostgrestSearchValue, which escapes %, _, and * as LIKE wildcard characters. Those characters have no special meaning inside a PostgreSQL double-quoted array element used with the cs (contains) operator — PostgreSQL stores them literally and matches them by equality. Escaping them inserts a literal backslash into the stored comparison value, so a tag named 100%off would be queried as 100\%off and silently fail to match any row. Only " and \ need to be escaped for the PostgreSQL array literal layer; ,, ., (, ), %, _, * are PostgREST filter-syntax and LIKE concerns that don't apply to the cs value.

Comment on lines +61 to 63
export function escapePostgrestArrayLiteralValue(value: string): string {
return escapePostgrestSearchValue(value).replace(/["{}]/g, (char) => `\\${char}`);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 escapePostgrestArrayLiteralValue has no direct unit tests in sanitize.test.ts

The new function is exported from sanitize.ts but sanitize.test.ts only imports and tests escapePostgrestSearchValue, sanitizeUrlParam, and sanitizeSearchParams. Coverage currently exists only through the integration-level candidates.test.ts. Adding a unit test alongside the existing escapePostgrestSearchValue block (e.g., covering backslash, quote, and brace combinations) would make it easier to iterate on the function in isolation.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Candidate search filters interpolate unescaped input

1 participant