Skip to content

fix(table): escape LIKE wildcards in $contains filter values#3949

Open
lawrence3699 wants to merge 1 commit intosimstudioai:mainfrom
lawrence3699:fix/escape-like-wildcards-in-contains-filter
Open

fix(table): escape LIKE wildcards in $contains filter values#3949
lawrence3699 wants to merge 1 commit intosimstudioai:mainfrom
lawrence3699:fix/escape-like-wildcards-in-contains-filter

Conversation

@lawrence3699
Copy link
Copy Markdown

Summary

The $contains filter operator in the table query builder (buildContainsClause) interpolates user-provided values directly into an ILIKE pattern without escaping LIKE wildcard characters (%, _, \).

This causes the filter to return incorrect, over-broad results when the search value contains these characters:

  • { name: { $contains: "100%" } } matches any row where name contains "100" followed by anything — not just the literal string "100%"
  • { name: { $contains: "user_name" } } matches "username", "user name", "userXname", etc. — because _ matches any single character in LIKE

Fix

Escape %, _, and \ in the value before interpolating into the ILIKE pattern, so they are treated as literal characters. PostgreSQL uses \ as the default LIKE escape character.

Test plan

  • Existing $contains tests in sql.test.ts continue to pass (values without wildcards are unaffected)
  • Verify that $contains: "100%" matches only rows containing the literal string "100%", not "100abc"
  • Verify that $contains: "a_b" matches only rows containing the literal string "a_b", not "axb"

The $contains filter operator builds an ILIKE pattern but does not
escape LIKE wildcard characters (%, _) in user-provided values.

This causes incorrect, over-broad query results when the search value
contains these characters. For example, filtering with
{ name: { $contains: "100%" } } matches any row where name
contains "100" followed by anything, not just the literal "100%".

Escape %, _, and \ in the value before interpolating into the ILIKE
pattern so that they match literally.
Copilot AI review requested due to automatic review settings April 4, 2026 22:51
@cursor
Copy link
Copy Markdown

cursor bot commented Apr 4, 2026

PR Summary

Low Risk
Low risk, localized change to SQL generation for $contains that only affects matching semantics when inputs include LIKE wildcard characters (%, _, \).

Overview
Fixes $contains filtering to treat LIKE wildcard characters as literals by escaping %, _, and \ before building the ILIKE '%...%' pattern.

Adds a small escapeLikePattern helper and updates buildContainsClause to use it, preventing over-broad matches when user input includes those characters.

Reviewed by Cursor Bugbot for commit b1790f3. Bugbot is set up for automated code reviews on this repo. Configure here.

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 4, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Apr 4, 2026 10:51pm

Request Review

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes $contains filtering in the table SQL query builder so user-provided values containing LIKE wildcards are treated literally (avoiding overly broad matches).

Changes:

  • Added escapeLikePattern() to escape %, _, and \ before building an ILIKE pattern.
  • Updated buildContainsClause() to use the escaped value when constructing the %...% match pattern.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 330 to 334
/** Builds case-insensitive pattern match: `data->>'field' ILIKE '%value%'` */
function buildContainsClause(tableName: string, field: string, value: string): SQL {
const escapedField = field.replace(/'/g, "''")
return sql`${sql.raw(`${tableName}.data->>'${escapedField}'`)} ILIKE ${`%${value}%`}`
return sql`${sql.raw(`${tableName}.data->>'${escapedField}'`)} ILIKE ${`%${escapeLikePattern(value)}%`}`
}
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

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

escapeLikePattern prefixes %, _, and \ with backslashes, but the generated ILIKE expression does not specify an ESCAPE clause. Adding ESCAPE '\\' makes the semantics explicit and matches the established pattern used elsewhere (e.g. apps/sim/lib/knowledge/documents/service.ts:895-907).

Copilot uses AI. Check for mistakes.
Comment on lines +325 to 334
/** Escapes LIKE/ILIKE wildcard characters so they match literally */
function escapeLikePattern(value: string): string {
return value.replace(/[\\%_]/g, '\\$&')
}

/** Builds case-insensitive pattern match: `data->>'field' ILIKE '%value%'` */
function buildContainsClause(tableName: string, field: string, value: string): SQL {
const escapedField = field.replace(/'/g, "''")
return sql`${sql.raw(`${tableName}.data->>'${escapedField}'`)} ILIKE ${`%${value}%`}`
return sql`${sql.raw(`${tableName}.data->>'${escapedField}'`)} ILIKE ${`%${escapeLikePattern(value)}%`}`
}
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

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

This change fixes wildcard handling for $contains, but there are no assertions covering the new escaping behavior. Please add unit tests (likely in apps/sim/lib/table/__tests__/sql.test.ts) that verify the generated pattern/params for inputs containing %, _, and \\ (e.g. 100%, a_b, c\\d) so regressions are caught.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit b1790f3. Configure here.

/** Escapes LIKE/ILIKE wildcard characters so they match literally */
function escapeLikePattern(value: string): string {
return value.replace(/[\\%_]/g, '\\$&')
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Duplicated escapeLikePattern utility across two modules

Low Severity

The new escapeLikePattern function in sql.ts is a semantic duplicate of the existing escapeLikePattern in apps/sim/lib/knowledge/documents/service.ts. Both escape the same three characters (\, %, _) for LIKE patterns, just with slightly different implementations (single regex vs. three chained .replace calls). Having two copies risks divergent bug fixes if one is updated without the other. This could be extracted to a shared utility.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b1790f3. Configure here.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 4, 2026

Greptile Summary

This PR correctly fixes a LIKE-wildcard injection bug in the $contains filter operator (buildContainsClause) by escaping %, _, and \ in the user-provided value before it is interpolated into the ILIKE pattern.

  • Fix is correct: the one-pass regex replacement (/[\\\\%_]/g'\\\\$&') produces valid PostgreSQL LIKE escape sequences (%\\%, _\\_, \\\\\\) without any double-escaping risk. Because Drizzle ORM passes the value as a bound parameter, no secondary SQL injection is possible, and PostgreSQL's default LIKE escape character (\\) is honoured correctly without requiring an explicit ESCAPE clause.
  • No new tests: the escapeLikePattern function and the wildcard input scenarios described in the PR test plan are not covered by any new test cases in sql.test.ts."

Confidence Score: 5/5

Safe to merge — the escaping logic is correct and the only finding is a missing unit test (P2).

The core fix is correct: the regex and replacement produce valid PostgreSQL LIKE escape sequences in a single pass, and Drizzle's parameterised query mechanism ensures no SQL injection is possible through the value. The only gap is that no unit tests were added for the new helper or for wildcard-containing inputs, which is a P2 quality concern that does not block merge.

Only apps/sim/lib/table/sql.ts is changed; __tests__/sql.test.ts would benefit from additional coverage but has no blocking issues.

Important Files Changed

Filename Overview
apps/sim/lib/table/sql.ts Adds escapeLikePattern helper that correctly escapes LIKE wildcards (%, _, \) via a single-pass regex before ILIKE interpolation; no unit tests added for the new function.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["buildFilterClause(filter, tableName)"] --> B{operator?}
    B -- "\$contains" --> C["buildContainsClause(tableName, field, value)"]
    C --> D["escapeLikePattern(value)"]
    D --> E["regex replace /[\\\\%_]/g with \\\\$&"]
    E --> F["e.g. '100%' → '100\\%',  'a_b' → 'a\\_b'", '"a\\\\b" → "a\\\\\\\\b"']
    F --> G["sql\`col ILIKE ${'%' + escaped + '%'}\`"]
    G --> H["Drizzle emits parameterized query\ncol ILIKE $1"]
    H --> I["PostgreSQL interprets \\% as literal %\n(default LIKE escape char = \\\\)"]
    I --> J["Only exact literal matches returned"]
Loading

Reviews (1): Last reviewed commit: "fix(table): escape LIKE wildcards in $co..." | Re-trigger Greptile

Comment on lines +325 to +333
/** Escapes LIKE/ILIKE wildcard characters so they match literally */
function escapeLikePattern(value: string): string {
return value.replace(/[\\%_]/g, '\\$&')
}

/** Builds case-insensitive pattern match: `data->>'field' ILIKE '%value%'` */
function buildContainsClause(tableName: string, field: string, value: string): SQL {
const escapedField = field.replace(/'/g, "''")
return sql`${sql.raw(`${tableName}.data->>'${escapedField}'`)} ILIKE ${`%${value}%`}`
return sql`${sql.raw(`${tableName}.data->>'${escapedField}'`)} ILIKE ${`%${escapeLikePattern(value)}%`}`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Missing tests for escapeLikePattern and wildcard inputs

The escapeLikePattern function is a new security-relevant helper, but no tests were added for it. The existing $contains test in __tests__/sql.test.ts (line 95) only asserts the result is defined — it does not verify that wildcards are actually escaped in the generated SQL pattern.

The PR description lists three concrete scenarios to verify ($contains: "100%", $contains: "a_b", $contains: "a\\b"), but none were implemented as test cases. Consider adding unit tests directly on escapeLikePattern covering these inputs:

describe('escapeLikePattern', () => {
  it('escapes percent signs', () => {
    expect(escapeLikePattern('100%')).toBe('100\\%')
  })
  it('escapes underscores', () => {
    expect(escapeLikePattern('a_b')).toBe('a\\_b')
  })
  it('escapes backslashes', () => {
    expect(escapeLikePattern('a\\b')).toBe('a\\\\b')
  })
  it('leaves plain strings unchanged', () => {
    expect(escapeLikePattern('john')).toBe('john')
  })
})

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.

2 participants