Fix for GHSA-h547-rmjf-5m2m

This commit is contained in:
Simon Willison 2026-09-16 16:43:48 -07:00
commit 23b76098f6
4 changed files with 52 additions and 1 deletions

View file

@ -343,7 +343,7 @@ def escape_css_string(s):
def escape_sqlite(s):
if _boring_keyword_re.match(s) and (s.lower() not in reserved_words):
if _boring_keyword_re.fullmatch(s) and (s.lower() not in reserved_words):
return s
elif "]" in s:
# SQLite does not support escaping ] inside [bracket] quoting, so fall

View file

@ -4,6 +4,13 @@
Changelog
=========
.. _v0_65_5:
0.65.5 (2026-09-16)
-------------------
- Fixed a security issue where a trailing newline in a requested table name could bypass table permissions and expose private rows. Thanks for the report, `dpfkdlemtp <https://github.com/dpfkdlemtp>`__. `GHSA-h547-rmjf-5m2m <https://github.com/simonw/datasette/security/advisories/GHSA-h547-rmjf-5m2m>`__
.. _v0_65_4:
0.65.4 (2026-09-10)

View file

@ -8,6 +8,48 @@ from datasette.app import Datasette
from datasette.plugins import pm
@pytest.mark.asyncio
@pytest.mark.parametrize("create_decoy", [False, True])
@pytest.mark.parametrize("path", [".json", "/1.json"])
async def test_trailing_lf_table_permissions(tmp_path, create_decoy, path):
db_path = tmp_path / "data.db"
conn = sqlite3.connect(db_path)
conn.executescript(
"create table secret (id integer primary key, value text);"
"insert into secret values (1, 'private');"
)
if create_decoy:
conn.executescript(
'create table "secret\n" (id integer primary key, value text);'
"insert into \"secret\n\" values (1, 'decoy');"
)
conn.close()
ds = Datasette(
[db_path],
settings={"default_allow_sql": False},
metadata={"databases": {"data": {"tables": {"secret": {"allow": False}}}}},
)
try:
response = await ds.client.get("/data.json?sql=select+*+from+secret")
assert response.status_code == 403
response = await ds.client.get("/data/secret" + path)
assert response.status_code == 403
# A trailing line feed must not bypass the protected table's permissions.
# Row URLs must also be safe when the suffixed table does not exist.
response = await ds.client.get("/data/secret~0A" + path + "?_shape=array")
if create_decoy:
assert response.status_code == 200, response.text
assert response.json() == [{"id": 1, "value": "decoy"}]
elif path == ".json":
assert response.status_code == 404
else:
assert response.status_code == 400
assert response.json()["error"] == "no such table: secret\n"
finally:
ds.executor.shutdown(wait=True)
@pytest.fixture
def ds(tmp_path):
path = tmp_path / "catalog.db"

View file

@ -216,6 +216,8 @@ def test_detect_fts(open_quote, close_quote):
"identifier,expected",
(
("plain", "plain"),
("plain\n", "[plain\n]"),
("select\n", "[select\n]"),
("select", "[select]"),
("has space", "[has space]"),
("has]bracket", '"has]bracket"'),