Skip to content

Commit dc3e418

Browse files
freekmurzeclaude
andcommitted
Strip MySQL fulltext boolean mode operators from search terms
Search terms are passed into a MySQL `MATCH(...) AGAINST(? IN BOOLEAN MODE)` query. In boolean mode MySQL has its own mini-syntax with the operators `+ - > < ( ) ~ * " @`. When user input contains one of these (for example the `@` proximity operator in a string like `icu4c@78`), MySQL's fulltext parser throws a 1064 syntax error instead of treating the input as data. `escapeSearchTerm()` only stripped `" * ( ) :`, leaving `@ + - ~ < >` intact. Add the remaining operators, and replace stripped operators with a space rather than removing them so each side becomes its own search term (every driver already splits the escaped term on whitespace before building its query). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 9bbee80 commit dc3e418

2 files changed

Lines changed: 24 additions & 8 deletions

File tree

src/Drivers/Database/Grammar.php

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,9 @@ abstract public function getTotalCount(Connection $connection, string $indexName
1414

1515
public function escapeSearchTerm(string $query): string
1616
{
17-
$escaped = str_replace(
18-
['"', '*', '(', ')', ':'],
19-
['', '', '', '', ''],
20-
$query
21-
);
17+
$booleanModeOperators = ['"', '*', '(', ')', ':', '@', '+', '-', '~', '<', '>'];
18+
19+
$escaped = str_replace($booleanModeOperators, ' ', $query);
2220

2321
$escaped = preg_replace('/\b(OR|AND|NOT)\b/i', '', $escaped);
2422

tests/Drivers/Database/GrammarTest.php

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,27 @@
77

88
it('escapes dangerous characters from search terms', function (Grammar $grammar) {
99
expect($grammar->escapeSearchTerm('"hello"'))->toBe('hello');
10-
expect($grammar->escapeSearchTerm('hello*world'))->toBe('helloworld');
11-
expect($grammar->escapeSearchTerm('test(group)'))->toBe('testgroup');
12-
expect($grammar->escapeSearchTerm('field:value'))->toBe('fieldvalue');
10+
expect($grammar->escapeSearchTerm('hello*world'))->toBe('hello world');
11+
expect($grammar->escapeSearchTerm('test(group)'))->toBe('test group');
12+
expect($grammar->escapeSearchTerm('field:value'))->toBe('field value');
13+
})->with([
14+
'sqlite' => fn () => new SqliteGrammar,
15+
'mysql' => fn () => new MySqlGrammar,
16+
'postgres' => fn () => new PostgresGrammar,
17+
]);
18+
19+
it('strips mysql boolean mode operators that would break the query', function (Grammar $grammar) {
20+
expect($grammar->escapeSearchTerm('icu4c@78'))->toBe('icu4c 78');
21+
expect($grammar->escapeSearchTerm('rank>up'))->toBe('rank up');
22+
expect($grammar->escapeSearchTerm('rank<down'))->toBe('rank down');
23+
expect($grammar->escapeSearchTerm('~negate'))->toBe('negate');
24+
25+
// The real-world path that triggered the crash on freek.dev should no longer
26+
// contain any boolean mode operator that breaks MySQL's fulltext parser.
27+
$path = '/System/Volumes/Preboot/Cryptexes/OS/opt/homebrew/opt/icu4c@78/lib/libicuio.78.dylib';
28+
expect($grammar->escapeSearchTerm($path))
29+
->not->toContain('@')
30+
->not->toContain('-');
1331
})->with([
1432
'sqlite' => fn () => new SqliteGrammar,
1533
'mysql' => fn () => new MySqlGrammar,

0 commit comments

Comments
 (0)