Back to Blog
Application Security

Datasette 1.0a39/0.65.4: A Case Study in Multi-Tenant Permission Bugs

Two patch releases close a cluster of subtle authorisation bypasses in the open-source data-publishing tool — a reminder that permission checks fail at the edges, not the middle.

PyramidLedger Research4 min read
Share

Key Takeaways

  • Datasette 1.0a39 and 0.65.4, released 11 September 2026, fix a cluster of permission-check bugs that could let a visitor with access to public tables read data from private ones in the same instance.
  • The bugs lived in edge cases: case-insensitive table-name matching, full-text-search index tables, SQLite's internal `sqlite_stat1`–`sqlite_stat4` tables, and joins performed via the `?_through=` filter all slipped past the intended permission model.
  • The fixes followed reports from external researcher Sevban Dönmez, then a week-long audit by maintainers Simon Willison and Alex Garcia using Claude Fable 5.1, GPT-5.6, and GPT-6 Astra.
  • Operators running Datasette on the public web with a mix of public and private tables should upgrade now; instances that are entirely public or entirely private are not the exposure this release targets.

What was released

On 11 September 2026, Datasette creator Simon Willison shipped two security patch releases: 1.0a39 for the project's alpha series and 0.65.4 for the stable 0.65.x line. Datasette is a widely used open-source tool for turning SQLite databases into browsable, queryable websites and JSON APIs, with a permission system that lets an operator mark some tables and databases private while keeping others public within the same deployment. According to the release announcement, the fixes matter most for instances running on the public web that mix both visibility levels — the exact configuration the bugs targeted.

Where the bugs actually lived

The GitHub release notes for 1.0a39 and 0.65.4 list a cluster of related permission-check fixes rather than one headline flaw:

  • Table and view permission checks now respect SQLite's case-insensitive name matching, closing a route where a differently-cased name could dodge the intended check.
  • Full-text-search index tables now require permission on the underlying source table before they can be viewed.
  • SQLite's internal statistics tables, sqlite_stat1 through sqlite_stat4, are denied by default.
  • Queries using the ?_through= filter, which joins through an intermediate table, now require permission on that intermediate table.
  • SQL-identifier and HTML escaping was hardened for table and column names pulled from untrusted database schemas.
  • Dynamic responses now set Cache-Control: private, no-store, and SQLite extension loading is disabled after startup.

Why this pattern is worth noting

None of these are the kind of bug a single access-control test catches. Each one is a derived surface that the core permission model didn't originally account for: a name compared the wrong way, a statistics table nobody thought to gate, a join path that reaches a table indirectly, an index that mirrors data from a table it doesn't inherit permissions from. This is the recurring shape of authorization bypass in any system that layers row- or table-level access control on top of a general-purpose query engine — the check has to be applied at every path that can reach the data, not just the obvious one. Teams building similar permission layers on top of SQL, GraphQL, or vector-store backends should read the fix list as a checklist of the edge cases their own model needs to cover.

An AI-assisted audit, done deliberately

The process behind the fix is also notable. After Sevban Dönmez reported the initial issues, Willison and co-maintainer Alex Garcia spent close to a week running an extended audit using three frontier models — Claude Fable 5.1, GPT-5.6, and GPT-6 Astra — cross-checking findings and reviewing each other's fixes rather than shipping on the first patch that closed a given report. That workflow — multiple models probing the same permission surface, followed by human review before release — is a reasonable template for open-source maintainers who don't have a dedicated security team but want more than a single pass at a fix.

What to do

  • If you run Datasette on the public internet and it serves a mix of public and private tables, upgrade to 1.0a39 or 0.65.4 immediately.
  • If your instance is entirely public, or entirely private with no unauthenticated access, the urgency is lower but the upgrade is still worth applying on your normal patch cycle.
  • If you've built a custom permission layer on top of Datasette or a similar SQL-publishing tool, use this fix list as a prompt to check your own handling of case sensitivity, derived/statistics tables, and indirect joins.

Frequently Asked Questions

Who is affected by the Datasette 1.0a39 and 0.65.4 security release?

Primarily operators running a public-facing Datasette instance that mixes public and private tables or databases behind Datasette's permission system. Purely public or purely private deployments face lower risk from these specific fixes.

Was there a single critical vulnerability, or several smaller ones?

Several related permission-check gaps, not one headline CVE: case-insensitive name matching, full-text-search index tables, SQLite's internal statistics tables, and joins through the `?_through=` filter each needed a separate fix, alongside some escaping hardening.

What's notable about how the fixes were produced?

The maintainers ran a roughly week-long audit using three different AI models — Claude Fable 5.1, GPT-5.6, and GPT-6 Astra — cross-checking each other's output before merging fixes, on top of the initial reports from an external researcher.

Sources

  1. 1Datasette 1.0a39 and 0.65.4 security releasesSimon Willison
  2. 2Release 0.65.4GitHub (simonw/datasette)
  3. 3Release 1.0a39GitHub (simonw/datasette)
Share

Read next