Database queries can search FTS5 tables on SQLite 3.46

FTS5 reads `PRAGMA data_version` through the authorizer when it opens a table
on SQLite 3.46 (Debian 13), so every MATCH and the schema endpoint failed with
"vtable constructor failed".  A denied VACUUM INTO also explains itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Roland 2026-10-11 17:15:30 -06:00
parent 5a054b97bc
commit 102aded9cc
No known key found for this signature in database
GPG key ID: 1BA620CE6C9A4680
2 changed files with 24 additions and 2 deletions

View file

@ -51,8 +51,10 @@ ALLOWED_ACTIONS = {
sqlite3.SQLITE_FUNCTION,
sqlite3.SQLITE_RECURSIVE,
}
# PRAGMAs which only describe the schema. A PRAGMA with an argument (e.g. `PRAGMA x=1`) is refused.
# PRAGMAs which only read, whatever their argument. Every other PRAGMA (e.g. `PRAGMA foreign_keys=OFF`) is refused.
ALLOWED_PRAGMAS = {
# FTS5 reads this when it opens a table, through the authorizer, on SQLite 3.46 (Debian 13).
'data_version',
'foreign_key_list',
'index_info',
'index_list',
@ -108,7 +110,7 @@ def _raise_query_error(e: sqlite3.Error):
message = str(e)
if isinstance(e, sqlite3.OperationalError) and message == 'interrupted':
message = f'Query exceeded the {QUERY_TIMEOUT:g} second time limit'
elif 'not authorized' in message:
elif 'not authorized' in message or 'authorization denied' in message:
message = 'Only read-only queries (SELECT) are allowed'
elif 'too big' in message:
message = f'A value is too large (over {MAX_VALUE_LENGTH} bytes)'

View file

@ -1,4 +1,5 @@
import json
import sqlite3
from http import HTTPStatus
from unittest import mock
@ -89,6 +90,7 @@ async def test_database_query_select(test_session, async_client, allow_database_
'PRAGMA foreign_keys=OFF',
'BEGIN',
'VACUUM',
"VACUUM INTO 'copy.db'",
"SELECT load_extension('evil')",
"SELECT 1; DELETE FROM tag",
])
@ -100,6 +102,8 @@ async def test_database_query_refuses_writes(test_session, async_client, allow_d
request, response = await post_query(async_client, sql)
assert response.status_code == HTTPStatus.BAD_REQUEST, response.json
assert response.json['code'] == 'INVALID_DATABASE_QUERY'
if ';' not in sql:
assert response.json['error'] == 'Only read-only queries (SELECT) are allowed'
request, response = await post_query(async_client, 'SELECT name FROM tag')
assert response.json['rows'] == [['one']]
@ -210,3 +214,19 @@ async def test_database_query_memory_limits(test_session, async_client, allow_da
assert response.status_code == HTTPStatus.OK, response.json
assert response.json['row_count'] == 3
assert response.json['truncated'] is True
@pytest.mark.asyncio
async def test_database_query_full_text_search(test_session, async_client, allow_database_query, video_factory):
"""FTS5 tables can be searched. FTS5 reads `PRAGMA data_version` through the authorizer on some SQLite
versions (3.46), so that PRAGMA must be allowed even where this SQLite does not ask."""
video_factory(title='Catching rain water')
test_session.commit()
sql = "SELECT fg.title FROM file_group_fts JOIN file_group fg ON fg.id = file_group_fts.rowid" \
" WHERE file_group_fts MATCH 'water'"
request, response = await post_query(async_client, sql)
assert response.status_code == HTTPStatus.OK, response.json
assert response.json['rows'] == [['Catching rain water']]
assert db_query._authorizer(sqlite3.SQLITE_PRAGMA, 'data_version', None, 'main', None) == sqlite3.SQLITE_OK