Deny unsupported write SQL operations by default

Require view-table permission for reads discovered inside write SQL analysis, including INSERT ... SELECT and CREATE TABLE ... AS SELECT.

Record additional SQLite authorizer callbacks as Operation values so unsupported functions, savepoints, virtual table DDL, and unknown callbacks are denied unless explicitly handled.
This commit is contained in:
Simon Willison 2026-05-27 14:52:52 -07:00
commit 86d0e7335f
6 changed files with 433 additions and 68 deletions

View file

@ -127,6 +127,100 @@ def test_analyze_transaction_operation(conn):
]
def test_analyze_savepoint_operation(conn):
analysis = analyze_sql_tables(conn, "savepoint s", database_name="data")
assert [operation_dict(operation) for operation in analysis.operations] == [
{
"operation": "savepoint",
"target_type": "transaction",
"database": None,
"sqlite_schema": None,
"table": None,
"target": "BEGIN s",
"columns": (),
"source": None,
"internal": False,
}
]
def test_analyze_function_operation(conn):
analysis = analyze_sql_tables(
conn,
"insert into dogs (name) values (upper(:name))",
{"name": "Cleo"},
database_name="data",
)
assert {
(
operation.operation,
operation.target_type,
operation.target,
operation.database,
operation.table,
)
for operation in analysis.operations
} == {
("insert", "table", "dogs", "data", "dogs"),
("function", "function", "upper", None, None),
("read", "table", "dogs", "data", "dogs"),
("update", "table", "cats", "data", "cats"),
("read", "table", "cats", "data", "cats"),
("insert", "table", "log", "data", "log"),
}
def test_analyze_create_virtual_table_operation():
conn = sqlite3.connect(":memory:")
try:
analysis = analyze_sql_tables(
conn,
"create virtual table docs using fts5(body)",
database_name="data",
)
finally:
conn.close()
assert {
"operation": "create",
"target_type": "virtual-table",
"database": "data",
"sqlite_schema": "main",
"table": "docs",
"target": "docs",
"columns": (),
"source": None,
"internal": False,
} in [operation_dict(operation) for operation in analysis.operations]
def test_analyze_create_table_as_select_function_is_not_internal():
conn = sqlite3.connect(":memory:")
try:
conn.execute("create table secret(value text)")
analysis = analyze_sql_tables(
conn,
"create table copied as select substr(value, 1, 1) from secret",
database_name="data",
)
finally:
conn.close()
assert {
"operation": "function",
"target_type": "function",
"database": None,
"sqlite_schema": None,
"table": None,
"target": "substr",
"columns": (),
"source": None,
"internal": False,
} in [operation_dict(operation) for operation in analysis.operations]
def test_analyze_insert_tables(conn):
analysis = analyze_sql_tables(
conn,