Fix for SQL injection issue in table filters, refs #2868

This commit is contained in:
Simon Willison 2026-08-06 10:22:35 -07:00
commit eb6c2b96b9
3 changed files with 94 additions and 27 deletions

View file

@ -66,12 +66,12 @@ from datasette.utils.asgi import Request
# JSON arraycontains, arraynotcontains
(
(("Availability+Info__arraycontains", "yes"),),
[":p0 in (select value from json_each([table].[Availability+Info]))"],
[':p0 in (select value from json_each("table"."Availability+Info"))'],
["yes"],
),
(
(("Availability+Info__arraynotcontains", "yes"),),
[":p0 not in (select value from json_each([table].[Availability+Info]))"],
[':p0 not in (select value from json_each("table"."Availability+Info"))'],
["yes"],
),
],
@ -83,6 +83,35 @@ def test_build_where(args, expected_where, expected_params):
assert {f"p{i}": param for i, param in enumerate(expected_params)} == actual_params
@pytest.mark.parametrize(
"key,expected_where",
(
(
'has"quote__exact',
'"has""quote" = :p0',
),
(
'has"quote__isnull',
'"has""quote" is null',
),
(
"has]bracket__arraycontains",
':p0 in (select value from json_each("table"."has]bracket"))',
),
),
)
def test_build_where_escapes_column_names(key, expected_where):
filters = Filters(((key, "value"),))
sql_bits, _ = filters.build_where_clauses("table")
assert sql_bits == [expected_where]
def test_build_where_escapes_table_name():
filters = Filters((("tags__arraycontains", "value"),))
sql_bits, _ = filters.build_where_clauses("items]bracket")
assert sql_bits == [':p0 in (select value from json_each("items]bracket"."tags"))']
@pytest.mark.asyncio
async def test_through_filters_from_request(ds_client):
request = Request.fake(

View file

@ -4,7 +4,7 @@ import urllib
import pytest
from datasette.fixtures import generate_compound_rows, generate_sortable_rows
from datasette.utils import detect_json1
from datasette.utils import detect_json1, tilde_encode
from datasette.utils.sqlite import sqlite_version
from .fixtures import make_app_client
@ -689,6 +689,34 @@ async def test_table_filter_queries_multiple_of_same_type(ds_client):
] == response.json()["rows"]
@pytest.mark.skipif(not detect_json1(), reason="Requires the SQLite json1 module")
def test_table_filters_quote_identifiers():
with make_app_client(
extra_databases={"demo.db": """
create table "items]bracket" (
id integer primary key,
"name""quote" text,
"tags]bracket" text
);
insert into "items]bracket" values (1, 'Alice', '["red"]');
"""},
) as client:
table_path = tilde_encode("items]bracket")
exact_query = urllib.parse.urlencode(
{'name"quote__exact': "Alice", "_shape": "arrays"}
)
exact_response = client.get(f"/demo/{table_path}.json?{exact_query}")
assert exact_response.status == 200
assert exact_response.json["rows"] == [[1, "Alice", '["red"]']]
array_query = urllib.parse.urlencode(
{"tags]bracket__arraycontains": "red", "_shape": "arrays"}
)
array_response = client.get(f"/demo/{table_path}.json?{array_query}")
assert array_response.status == 200
assert array_response.json["rows"] == [[1, "Alice", '["red"]']]
@pytest.mark.skipif(not detect_json1(), reason="Requires the SQLite json1 module")
@pytest.mark.asyncio
async def test_table_filter_json_arraycontains(ds_client):