Datasette 0.65.3 closes a filter identifier injection gap
Datasette 0.65.3 fixes SQL injection through table filters that could expose private tables. Parameterized values still require correctly quoted table and column identifiers.
Datasette 0.65.3, released on 6 August 2026, fixes SQL injection affecting instances that expose public and private tables within the same SQLite database. A user allowed to read a public table could bypass the intended restriction and read private data through the vulnerable filtering path. The release announcement says the fix is also available in 1.0a38. This is a Datasette application issue, so replacing the underlying SQLite library is not the corrective action described by the release.
I have used SQLite for twenty-five years, and this site runs on it. I like the way an embedded engine keeps the database deployment proportionate to the application. A web application that publishes selected parts of a database still has to enforce that selection. The engine can execute a syntactically valid query without knowing which HTTP actor should have been allowed to construct it.
The permission boundary crosses generated SQL
The announcement addresses a particular deployment arrangement. An administrator serves some tables publicly, keeps others private, and disables arbitrary SQL access to prevent readers querying the private tables directly. The vulnerability allowed public-table filters to undermine that arrangement. Its described effect is read-only access to private data in the same database, which is already a confidentiality failure.
Datasette's permission documentation at this release defines permissions in terms of an actor performing an action on a resource. execute-sql controls arbitrary queries against a database. Table viewing is a different action. That division permits a useful product behavior, but it requires every query-generation path behind table viewing to remain within the permission granted for that table.
I would review that dependency whenever an interface offers a constrained query builder alongside a more powerful SQL console. Disabling the console answers the question of whether the actor may submit SQL directly. The filter builder still generates SQL on the actor's behalf. Its implementation has to preserve the narrower operation that the interface promises.
For example, a permission test that requests a private table directly can pass while a filter on a public table remains vulnerable. Both requests reach database code, but they exercise different authorization and query-construction paths. A meaningful security regression test needs to represent the actual route through which the restricted actor can act.
Values and identifiers occupy different SQL positions
The filter patch changes how TemplatedFilter handles table and column names. It quotes those identifiers before substituting them into the SQL template. The values continue to use named parameters. This is a useful example of why a query containing parameters can still have an injection defect elsewhere in its construction.
SQLite's binding documentation describes replacing literal values with parameters whose contents are supplied separately. A filter value belongs in that mechanism. A table or column name occupies an identifier position in the SQL grammar, so the query builder must handle its construction separately.
For an illustrative equality filter, the value can remain outside the SQL text while the column name appears as a quoted identifier. This fragment assumes an existing table with the named column and a binding for :value.
SELECT *
FROM "public_items"
WHERE "display_name" = :value;
The distinction is structural. Binding a value does not validate or quote some other piece of text already interpolated into the statement. I would inspect each source of generated SQL by its grammatical role, including the names selected by sorting or filtering controls.
That review should also establish which names the actor is allowed to select. Quoting makes a name an identifier with the intended spelling. Authorization decides whether the actor may use that identifier in the operation. A query builder needs both properties when it constructs queries over resources with different access rules.
Quoting must handle the delimiter inside the name
The utility change addresses names containing a closing square bracket. Its identifier escape helper falls back to double-quote quoting for those names and doubles embedded double quotes. Wrapping arbitrary text between delimiter characters is insufficient when the text can itself contain the closing delimiter.
SQLite documents the accepted identifier quoting forms in its keyword reference. Double quotes delimit identifiers, and square brackets are a compatibility form. The reference also cautions against depending on historical exceptions that reinterpret incorrectly quoted tokens. A query generator should produce the intended grammar without asking those exceptions to infer its meaning.
This matters for legitimate data as well as hostile inputs. A table imported from another system may have punctuation in its column names. The correct outcome is that the application either supports that identifier safely or rejects it according to an explicit policy. Accidentally turning part of the name into SQL syntax is a separate behavior that neither outcome permits.
The corrected filter implementation centralizes quoting before template formatting. That makes the template's inputs easier to reason about because the identifier slots receive SQL identifier expressions, while parameter slots retain their separate purpose. I would use that distinction when reviewing any new filter type added later.
Regression coverage should include valid unusual names
The release's filter tests add cases for quoted column names and bracket-containing identifiers. The table API tests exercise requests against tables with those names. That combination checks the SQL construction and the behavior reached through an HTTP request.
I would keep both kinds of test. A unit assertion can identify an escaping regression precisely. A request-level test can catch mistakes in how request parameters become the inputs to that helper. Removing either leaves a different part of the path unexplained when a future refactor changes the result.
For a mixed public/private deployment, I would add an application-level assertion that the restricted actor cannot obtain a known private marker through a public-table request. The fixture should also verify that ordinary public filtering still works. Otherwise a test might accept a change that prevents disclosure simply by breaking all table queries.
The expected outcome should distinguish an invalid filter from an authorized query returning no matches. Both can produce no useful rows, but they represent different application behavior. Recording status and response semantics alongside the absence of private data makes the test less ambiguous.
Upgrade the application that generates the query
The announcement recommends disabling execute-sql for the mixed-table arrangement and explains that the bug bypassed that restriction. I would retain the restriction after upgrading. The repaired filter path and the arbitrary-query permission address different ways of reaching the private tables, so the application still needs its intended permission configuration.
I would also check every deployed Datasette instance serving the affected arrangement. Updating a development utility does not establish that the public instance uses the repaired package. The deployment record should identify the running application version and the permission configuration used in its request-level checks.
An operator reviewing possible historical exposure needs to distinguish the vulnerability's documented capability from evidence about actual requests. The release establishes that private reads were possible under the described conditions. It does not establish that a particular installation's data was accessed. I would preserve the relevant request records and avoid treating a successful upgrade as an answer to that historical question.
I would include filters that take no value in the regression inventory. The patch also quotes identifiers in that branch of TemplatedFilter, which handles operations such as testing whether a column is null. A review limited to equality filters with supplied values would overlook it. The security property concerns construction of the column expression regardless of whether the operation requires a parameter.
Array membership deserves a separate case because its template names both the table and the column inside a JSON iteration expression. Testing an ordinary scalar comparison does not exercise that qualified-name construction. The new cases demonstrate why the test inputs should follow the supported filter families instead of varying only the value in one family.
The deployment check I would retain is specific to the application's access model. A public actor should still receive the expected public rows through supported filters, while private rows remain inaccessible through those same routes. The test should exercise the running Datasette version and the actual permission rules that the installation relies on.