fix(data-access): support case-insensitive key filters for PostgREST queries#1388
fix(data-access): support case-insensitive key filters for PostgREST queries#1388solaris007 wants to merge 3 commits intomainfrom
Conversation
…queries After switching to PostgREST, URL lookups became case-sensitive because #applyKeyFilters always used .eq() which maps to PostgreSQL's case-sensitive equality. This adds a caseInsensitive schema attribute property that makes #applyKeyFilters use .ilike() instead, restoring the case-insensitive behavior for baseURL lookups.
|
This PR will trigger a patch release when merged. |
ILIKE treats % and _ as wildcards, so without escaping, a value like 'my_site.com' would match 'myXsite.com'. Escape these characters before passing to ilike so it behaves as case-insensitive equality.
| const dbField = this.#toDbField(key); | ||
| const attr = this.schema.getAttribute(key); | ||
| if (attr?.caseInsensitive && typeof value === 'string') { | ||
| const escaped = value.replace(/[%_]/g, '\\$&'); |
Check failure
Code scanning / CodeQL
Incomplete string escaping or encoding High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 2 months ago
In general, when escaping characters for a pattern language (like SQL LIKE), you must escape not only the special pattern characters (here % and _) but also the escape character itself (commonly \). Otherwise, an attacker can prepend a backslash to a wildcard so that your escaping turns \% into \\%, which may be interpreted as an escaped backslash followed by a wildcard, reintroducing special meaning for %. The safest approach is to first escape all backslashes, then escape % and _, using a global regular expression for each.
The best targeted fix here is to adjust the escaping logic on line 384 to escape backslashes as well. To preserve existing behavior while making it correct, we can transform the value in two steps: replace each single backslash with a double backslash, then escape % and _ by prefixing them with backslashes. Doing backslashes first avoids re-escaping the ones we add when escaping % and _. Concretely, in #applyKeyFilters, change the single value.replace(/[%_]/g, '\\$&') call to a small sequence:
const escaped = value
.replace(/\\/g, '\\\\')
.replace(/[%_]/g, '\\$&');This keeps everything local to this function, introduces no behavior changes beyond correctly handling backslashes, and requires no new imports or additional helpers. Only the body of the if (attr?.caseInsensitive && typeof value === 'string') block needs to be updated.
| @@ -381,7 +381,9 @@ | ||
| const dbField = this.#toDbField(key); | ||
| const attr = this.schema.getAttribute(key); | ||
| if (attr?.caseInsensitive && typeof value === 'string') { | ||
| const escaped = value.replace(/[%_]/g, '\\$&'); | ||
| const escaped = value | ||
| .replace(/\\/g, '\\\\') | ||
| .replace(/[%_]/g, '\\$&'); | ||
| filtered = filtered.ilike(dbField, escaped); | ||
| } else { | ||
| filtered = filtered.eq(dbField, value); |
|
@solaris007 Nice catch on the case-sensitivity regression. The fix works, but I'm concerned about the A simpler alternative is to handle this at the database level with CREATE EXTENSION IF NOT EXISTS citext;
ALTER TABLE sites ALTER COLUMN base_url TYPE citext;This makes the
I can create the migration on the data-service side. What do you think? |
|
closing as per #1388 (comment) |
Summary
#applyKeyFiltersinBaseCollectionalways used.eq(), which maps to PostgreSQL's case-sensitive equality operatorcaseInsensitiveschema attribute property that makes#applyKeyFiltersuse.ilike()instead of.eq()for marked fieldsbaseURLin the Site schema withcaseInsensitive: true, restoring case-insensitive behavior for all auto-generated accessors (findByBaseURL,allByBaseURL)Test plan
.ilike()is used forcaseInsensitiveattributes and.eq()for regular ones