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.
This commit is contained in:
Bunlong Heng 2026-08-12 01:48:06 -04:00 committed by GitHub
commit 2d3c6b9a1e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 15 additions and 1 deletions

View file

@ -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

View file

@ -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}"