indexer: escape ILIKE wildcards in pair search (#459) #984

Merged
Brouie merged 1 commit from qa/459-ilike-escape into main 2026-06-30 22:00:01 +00:00
Brouie commented 2026-06-30 19:06:09 +00:00 (Migrated from gitlab.com)

GET /api/v1/pairs?q= embedded the user query into ILIKE patterns via
format!("%{}%", q) with no metacharacter escaping. ?q=% produced ILIKE '%%%'
and matched every pair (and forced a sequential scan since leading-wildcard
ILIKE can't use the index); ?q=_ matched any single character. The 128-char
truncation didn't help — a burst of one-char/wildcard queries is a search
amplification vector against Postgres CPU (SEC-I04 F02).

Add escape_like_pattern() — escapes , %, _ (backslash first to avoid
double-escaping). Postgres' default LIKE/ILIKE escape char is backslash, so the
bound pattern needs no explicit ESCAPE clause. Applied at every pattern-building
site (relevance score, list filters, and the split pair-symbol sub-tokens).
Single-char symbol search still works (escaping, not a min-length reject).

Tests: 4 unit tests on the escaper + 1 Postgres integration test proving
?q=% and ?q=_ return empty while ?q=LUNC still matches; api_pairs suite 16/0
(incl relevance ordering).

GET /api/v1/pairs?q= embedded the user query into ILIKE patterns via format!("%{}%", q) with no metacharacter escaping. ?q=% produced ILIKE '%%%' and matched every pair (and forced a sequential scan since leading-wildcard ILIKE can't use the index); ?q=_ matched any single character. The 128-char truncation didn't help — a burst of one-char/wildcard queries is a search amplification vector against Postgres CPU (SEC-I04 F02). Add escape_like_pattern() — escapes \, %, _ (backslash first to avoid double-escaping). Postgres' default LIKE/ILIKE escape char is backslash, so the bound pattern needs no explicit ESCAPE clause. Applied at every pattern-building site (relevance score, list filters, and the split pair-symbol sub-tokens). Single-char symbol search still works (escaping, not a min-length reject). Tests: 4 unit tests on the escaper + 1 Postgres integration test proving ?q=% and ?q=_ return empty while ?q=LUNC still matches; api_pairs suite 16/0 (incl relevance ordering).
Brouie commented 2026-06-30 19:06:09 +00:00 (Migrated from gitlab.com)

assigned to @Brouie

assigned to @Brouie
Brouie commented 2026-06-30 19:08:41 +00:00 (Migrated from gitlab.com)

mentioned in issue #459

mentioned in issue #459
PlasticDigits commented 2026-06-30 19:09:13 +00:00 (Migrated from gitlab.com)

Security review — MR !984

Commit reviewed: ef0c850d75cca786472bc252fa0bde21ef947b6a
Scope: escape_like_pattern() applied to all GET /api/v1/pairs?q= ILIKE pattern construction sites (push_pair_relevance_score, push_pair_list_filters, pair-symbol sub-tokens); unit + integration tests for % / _ neutralization.

Outcome: FINDINGS: 0 medium+

Method

  • Traced attacker-controlled ?q= from list_pairs (PAIR_LIST_Q_MAX_LEN=128 truncation) through count_pairs_filtered / list_pairs_filtered into push_pair_list_filters and push_pair_relevance_score.
  • Verified patterns are bound via sqlx::QueryBuilder::push_bind (parameterized — no SQL injection sink).
  • Confirmed escape_like_pattern neutralizes \, %, _ (backslash first) before wrapping with %…%, closing SEC-I04 F02 search-amplification (?q=% → match-all + sequential scan; ?q=_ → single-char wildcard).
  • Checked prior MR discussion threads: no prior security-review notes to re-validate.

Assessment

This diff remediates the reported vulnerability rather than introducing new attack surface. Escaping aligns with PostgreSQL’s default LIKE/ILIKE backslash escape semantics on bound parameter values; integration test search_wildcard_query_does_not_match_all_pairs exercises the fix end-to-end.

Residual leading-wildcard cost on normal substring searches (%term%) is pre-existing intentional behavior and out of scope for this diff.

Security review: no medium+ findings on this diff.

## Security review — MR !984 **Commit reviewed:** `ef0c850d75cca786472bc252fa0bde21ef947b6a` **Scope:** `escape_like_pattern()` applied to all `GET /api/v1/pairs?q=` ILIKE pattern construction sites (`push_pair_relevance_score`, `push_pair_list_filters`, pair-symbol sub-tokens); unit + integration tests for `%` / `_` neutralization. **Outcome:** `FINDINGS: 0` medium+ ### Method - Traced attacker-controlled `?q=` from `list_pairs` (`PAIR_LIST_Q_MAX_LEN=128` truncation) through `count_pairs_filtered` / `list_pairs_filtered` into `push_pair_list_filters` and `push_pair_relevance_score`. - Verified patterns are bound via `sqlx::QueryBuilder::push_bind` (parameterized — no SQL injection sink). - Confirmed `escape_like_pattern` neutralizes `\`, `%`, `_` (backslash first) before wrapping with `%…%`, closing SEC-I04 F02 search-amplification (`?q=%` → match-all + sequential scan; `?q=_` → single-char wildcard). - Checked prior MR discussion threads: no prior security-review notes to re-validate. ### Assessment This diff **remediates** the reported vulnerability rather than introducing new attack surface. Escaping aligns with PostgreSQL’s default LIKE/ILIKE backslash escape semantics on bound parameter values; integration test `search_wildcard_query_does_not_match_all_pairs` exercises the fix end-to-end. Residual leading-wildcard cost on normal substring searches (`%term%`) is pre-existing intentional behavior and out of scope for this diff. **Security review: no medium+ findings on this diff.**
PlasticDigits (Migrated from gitlab.com) merged commit 16cdb4336b into main 2026-06-30 22:00:01 +00:00
PlasticDigits commented 2026-06-30 22:00:29 +00:00 (Migrated from gitlab.com)

mentioned in commit 16cdb4336b

mentioned in commit 16cdb4336b481962713c68df68cefdc73f2f81e8
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
code/cl8y-dex-terraclassic!984
No description provided.