Disallow ?_through= if user lacks view-table permission

Refs GHSA-53fc-rhfg-h7qp

Co-authored-by: Alex Garcia <15178711+asg017@users.noreply.github.com>
This commit is contained in:
Simon Willison 2026-09-03 14:35:45 -07:00
commit 577aeb73f0
2 changed files with 37 additions and 1 deletions

View file

@ -2,7 +2,7 @@ import json
from typing import ClassVar from typing import ClassVar
from datasette import hookimpl from datasette import hookimpl
from datasette.resources import DatabaseResource from datasette.resources import DatabaseResource, TableResource
from datasette.utils.asgi import BadRequest from datasette.utils.asgi import BadRequest
from datasette.views.base import DatasetteError from datasette.views.base import DatasetteError
@ -135,6 +135,11 @@ def through_filters(request, database, table, datasette):
through_table = through_data["table"] through_table = through_data["table"]
other_column = through_data["column"] other_column = through_data["column"]
value = through_data["value"] value = through_data["value"]
await datasette.ensure_permission(
action="view-table",
resource=TableResource(database=database, table=through_table),
actor=request.actor,
)
db = datasette.get_database(database) db = datasette.get_database(database)
outgoing_foreign_keys = await db.foreign_keys_for_table(through_table) outgoing_foreign_keys = await db.foreign_keys_for_table(through_table)
fk_to_us = next( fk_to_us = next(

View file

@ -1778,3 +1778,34 @@ async def test_next_url_included_by_default(ds_client):
data = response.json() data = response.json()
assert data["next"] is None assert data["next"] is None
assert data["next_url"] is None assert data["next_url"] is None
@pytest.mark.asyncio
async def test_table_through_requires_view_table_on_through_table():
# GHSA-53fc-rhfg-h7qp issue 3: ?_through= runs a sub-select against the
# caller-supplied through table, so the actor must be allowed to view it.
# Otherwise it is an equality oracle over any column of a denied table.
from datasette.app import Datasette
ds = Datasette(
memory=True,
config={"databases": {"data": {"tables": {"salaries": {"allow": False}}}}},
)
db = ds.add_memory_database("table_through_denied", name="data")
await db.execute_write("create table people (id integer primary key, name text)")
await db.execute_write(
"create table salaries (id integer primary key, "
"person_id integer references people(id), note text)"
)
await db.execute_write("insert into people values (1, 'alice'), (2, 'bob')")
await db.execute_write("insert into salaries values (1, 1, 'TOPSECRET-A')")
await ds.invoke_startup()
# Sanity: anonymous cannot read salaries directly
assert (await ds.client.get("/data/salaries.json")).status_code == 403
response = await ds.client.get(
"/data/people.json?_shape=array"
'&_through={"table":"salaries","column":"note","value":"TOPSECRET-A"}'
)
assert response.status_code == 403, response.text