From 3ae092896d59887c39def82e6e49ec39e143f358 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Thu, 3 Sep 2026 14:36:33 -0700 Subject: [PATCH] Only allow /db/name/-/schema against tables and views Refs GHSA-926p-cw2f-643h Co-authored-by: Alex Garcia <15178711+asg017@users.noreply.github.com> --- datasette/views/special.py | 3 ++- tests/test_schema_endpoints.py | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/datasette/views/special.py b/datasette/views/special.py index 435d5583..d3ccbebc 100644 --- a/datasette/views/special.py +++ b/datasette/views/special.py @@ -1418,7 +1418,8 @@ class TableSchemaView(SchemaBaseView): # Get schema for the table db = self.ds.databases[database_name] result = await db.execute( - "select sql from sqlite_master where name = ? and sql is not null", + "select sql from sqlite_master where name = ? " + "and type in ('table', 'view') and sql is not null", [table_name], ) row = result.first() diff --git a/tests/test_schema_endpoints.py b/tests/test_schema_endpoints.py index d3ed0477..5ed2c12a 100644 --- a/tests/test_schema_endpoints.py +++ b/tests/test_schema_endpoints.py @@ -333,3 +333,27 @@ async def test_schema_parent_views_hide_denied_tables( assert "CREATE TABLE employee_salaries" in response.text assert "idx_employee_salaries_ssn" in response.text assert "trg_employee_salaries" in response.text + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "object_name", ["idx_employee_salaries_ssn", "trg_employee_salaries"] +) +@pytest.mark.parametrize("format_ext", ["json", "md", ""]) +async def test_table_schema_does_not_serve_objects_of_denied_table( + schema_table_perms_ds, object_name, format_ext +): + """ + Related to GHSA-926p-cw2f-643h: /db//-/schema looks up sqlite_master + by name without restricting to tables/views, so requesting the name of an + index or trigger that belongs to a denied table serves its DDL. The + view-table check runs against the index/trigger name, which is not a + restricted table, so it passes. + """ + url = f"/schema_table_perms_db/{object_name}/-/schema" + if format_ext: + url += f".{format_ext}" + response = await schema_table_perms_ds.client.get(url) + assert response.status_code in (403, 404) + assert "employee_salaries" not in response.text + assert "ssn" not in response.text