ReDoS (CWE-1333): Bad backtracking in `like()`/`ilike()` query LIKE pattern compilation
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 3.9k
- Forks
- 266
- Avg merge
- 1d 4h
- Merged PRs (30d)
- 55
Description
The problem
The like() and ilike() query builder functions convert SQL LIKE wildcards (% → .*) into a RegExp without bounds. A crafted pattern with many % wildcards creates overlapping .* segments that cause catastrophic backtracking on near-matching inputs.
Vulnerable code
File: packages/db/src/query/compiler/evaluators.ts (lines 632-655)
function evaluateLike(
value: any,
pattern: any,
caseInsensitive: boolean,
): boolean {
if (typeof value !== `string` || typeof pattern !== `string`) {
return false
}
const searchValue = caseInsensitive ? value.toLowerCase() : value
const searchPattern = caseInsensitive ? pattern.toLowerCase() : pattern
// Convert SQL LIKE pattern to regex
// First escape all regex special chars except % and _
let regexPattern = searchPattern.replace(/[.*+?^${}()|[\]\\]/g, `\\$&`)
// Then convert SQL wildcards to regex
regexPattern = regexPattern.replace(/%/g, `.*`) // % matches any sequence
regexPattern = regexPattern.replace(/_/g, `.`) // _ matches any single char
// 's' (dotAll flag) makes '.' match all characters including line terminations
const regex = new RegExp(`^${regexPattern}$`, 's')
return regex.test(searchValue)
}
PoC
import { createCollection, createLiveQueryCollection, like } from '@tanstack/db'
const collection = createCollection({
name: 'items',
schema: { name: 'string' },
sync: { type: 'none' },
})
const liveQuery = createLiveQueryCollection({
source: collection,
query: (q) => q.where(({ item }) => like(item.name, 'a%a%a%a%a%a%a%a%a%a%a%a%a%a%a%a%a%a%a%a')),
})
// Near-miss: long string that matches most of the pattern but fails at the end
// The 'b' at the end forces the engine into exponential backtracking
const nearMiss = 'a' + 'a'.repeat(50) + 'b'
const start = Date.now()
await liveQuery.refetch()
console.log(`Took ${Date.now() - start}ms`) // seconds to minutes
Why it's exploitable
like()/ilike()are part of the public query builder API, any app using TanStack/db can call them- The pattern string is accepted as
StringLike(barestring) with zero validation - Each
%becomes.*, creating N+1 overlapping unbounded quantifier segments - When tested against a near-miss string (long, matches most of the pattern but fails near the end), the regex engine tries all possible ways to distribute the input across the segments → exponential time
Remediation
Quick fix : limit wildcards in evaluateLike():
const MAX_WILDCARDS = 20
if ((pattern as string).match(/%/g)?.length > MAX_WILDCARDS) {
throw new Error('LIKE pattern contains too many wildcards')
}
Long-term : replace regex-based LIKE with a manual character-by-character matcher (e.g. KMP-style) for O(n*m) worst-case.
Submitting
Per TanStack's security advisory process, I'd like to submit a GHSA for this. Happy to provide a more detailed report or PoC video if needed.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in packages/db/src/query/compiler/evaluators.ts at evaluateLike() and reproduce the provided near-miss pattern to confirm the slowdown. Review the wildcard conversion and preserve existing LIKE/ILIKE behavior while ensuring crafted patterns cannot trigger catastrophic backtracking; done means the PoC completes promptly and matching behavior remains covered by tests.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- databases, security
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 64/100