From 2d3c6b9a1e5068fcee6923c9ed74cbd158ee9db4 Mon Sep 17 00:00:00 2001 From: Bunlong Heng Date: Wed, 12 Aug 2026 01:48:06 -0400 Subject: [PATCH] Escape tokenize argument in enable_fts (#828) The tokenize value passed to Table.enable_fts() was interpolated directly into the CREATE VIRTUAL TABLE statement inside a single-quoted string literal. A value containing a single quote could break out of that literal and inject arbitrary SQL, which executes via executescript(). This is reachable from the CLI via 'enable-fts --tokenize'. Route the value through the existing Database.quote() helper so SQLite itself escapes it. Legitimate tokenizers such as 'porter' are unaffected. Adds a regression test. --- sqlite_utils/db.py | 4 +++- tests/test_fts.py | 12 ++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/sqlite_utils/db.py b/sqlite_utils/db.py index 2e9b570..45482f3 100644 --- a/sqlite_utils/db.py +++ b/sqlite_utils/db.py @@ -3514,7 +3514,9 @@ class Table(Queryable): table_fts=quote_identifier(self.name + "_fts"), columns=", ".join(quote_identifier(c) for c in columns), fts_version=fts_version, - tokenize=f"\n tokenize='{tokenize}'," if tokenize else "", + tokenize=( + f"\n tokenize={self.db.quote(tokenize)}," if tokenize else "" + ), ) ) should_recreate = False diff --git a/tests/test_fts.py b/tests/test_fts.py index 50c1770..395fc66 100644 --- a/tests/test_fts.py +++ b/tests/test_fts.py @@ -252,6 +252,18 @@ def test_fts_tokenize(fresh_db, fts_version): }.items() <= rows[0].items() +def test_fts_tokenize_escaped(fresh_db): + # A malicious tokenize value must not be able to break out of the + # string literal in the CREATE VIRTUAL TABLE statement. + table = fresh_db["searchable"] + table.insert_all(search_records) + malicious = "porter'); CREATE TABLE injected(x); --" + with pytest.raises(Exception): + table.enable_fts(["text"], tokenize=malicious) + # The injected statement must not have executed + assert "injected" not in fresh_db.table_names() + + def test_optimize_fts(fresh_db): for fts_version in ("4", "5"): table_name = f"searchable_{fts_version}"