Fix the Datasette SQL Injection: Why execute-sql Won't Save You
The Datasette SQL injection patched in 1.0a38 and 0.65.3 leaks private tables via unescaped filter columns. The check, upgrade, and why execute-sql is no fix.

TL;DR
The Datasette SQL injection is a High-severity flaw in query-string table filters (GHSA-w3hf-fcg5-p4cc, CVSS 7.5) that lets anyone with access to a single public table read restricted tables in the same SQLite file. It was patched in Datasette 1.0a38 and 0.65.3, both shipped on 6 August 2026; the filters interpolated column and table identifiers into generated SQL without quoting them, so parameter binding never applied and the execute-sql permission was never in the path. Upgrade, then stop treating table-level permissions as a boundary between trust levels in one database file.
Am I affected by the Datasette SQL injection? The two-minute check
There are exactly two affected ranges, and they map to Datasette’s two live branches:
| Branch | Affected | Patched |
|---|---|---|
| Stable | < 0.65.3 | 0.65.3 |
| 1.0 alpha | >= 1.0a0, < 1.0a38 | 1.0a38 |
Check what you are running and move:
datasette --version
# stable branch
pip install -U 'datasette>=0.65.3'
# 1.0 alpha branch
pip install -U 'datasette>=1.0a38'Exposure needs one more condition on top of an affected version: a database file that mixes tables an untrusted audience can reach with tables it cannot. If every table in the file is public, an attacker gains nothing they did not already have. If the file is entirely private and unreachable without authentication, the entry point is gone. The vulnerable shape is the middle case — one SQLite file, some tables published, some held back by the Datasette permissions system.
That combination is uncommon in practice — Simon Willison says as much in the release note for 1.0a38 — but “uncommon” is not “rare enough to skip”. Published-data sites are precisely where somebody stages one file with a public summary table and a private raw table beside it.
What actually broke: identifiers, not values
Every developer who has been told “use parameterised queries” has absorbed half the lesson. Binding protects values. It cannot protect identifiers, because SQLite has no placeholder for a table or column name. You can write this:
SELECT * FROM events WHERE price > ?You cannot write this:
SELECT * FROM ? WHERE ? > ? -- identifiers are not bindableSo any application that lets a user pick which column to filter on has to escape that identifier itself. Datasette does exactly that by design: its table filter parameters are named after columns. ?price__gt=5 means “the column price, greater than the value 5”. The value went through binding. The column name went into the SQL string.
The advisory’s own summary of the fix is the whole story in one sentence: it “ensures that table and column identifiers are safely quoted before they are incorporated into generated SQL.” Before the patch, the built-in filter templates took an identifier straight out of the query string and pasted it into the WHERE clause. An attacker who controls that identifier controls a fragment of the SQL, and a fragment is enough to append a subquery against a table the permission layer meant to hide.
I am describing the class of bug, not publishing a payload — the mechanism is enough to reason about your own code, and geo-chen, who reported it, has earned the right to let the patch land first.
The severity numbers follow from that path. CVSS v3.1 scores it 7.5: network attack vector, no privileges required, no user interaction, high confidentiality impact, no integrity or availability impact. “No privileges required” is not an exaggeration — on a public Datasette instance the attacker is an anonymous visitor loading a table page, and read-only is the ceiling because the injection lands in a SELECT.
Why execute-sql is not the fix
The release note advises administrators serving private tables to disable the execute-sql permission on that database. That is good advice for a different problem, and it is worth doing. It is not a mitigation for this one, and the advisory says so directly: disabling execute-sql alone provides no protection.
The reason is structural. execute-sql guards the arbitrary-SQL entry point — the box where a user types a query. This bug never touches that entry point. It rides in on ?column__gt=, a parameter that exists on every table page and is gated only by whether you can view that table. Turning off the front door does nothing about a window in the wall next to it.
This is worth sitting with, because the same shape shows up in most permission systems built on top of a query layer: the check runs on the route, and the SQL is composed after the check. Anything that can influence SQL composition after authorisation has already run is outside the boundary you thought you drew. It is the same failure mode I wrote about in locking down MCP write tools — the tool definition looks constrained, and the constraint lives one layer above where the damage happens.
The durable fix: the database file is the trust boundary
The advisory’s workaround for anyone who cannot upgrade is to stop mixing tables of different sensitivity in the same file. Keep that advice after you patch, because it is the only boundary in this stack that does not depend on a code path being correct.
Table-level permissions are an application-layer convention. SQLite has no idea they exist; ATTACH, subqueries, and sqlite_master all operate on the whole file. When your isolation is enforced only by Python code that decides which SQL to compose, every bug in composition is a permission bypass. When your isolation is “the sensitive rows live in private.db, and the untrusted audience has no permission to open that file”, a filter-quoting bug is a filter-quoting bug.
The cost is real: two files mean no cross-database joins without ATTACH, and some duplication of reference tables. Pay it for anything where the private side would be a disclosure incident. That is the same reasoning behind separating access at the account level rather than the row level in offboarding controls — coarse boundaries survive bugs that fine ones do not.
No CVE, which is the operational catch
At publication there is no CVE assigned. The issue exists as GHSA-w3hf-fcg5-p4cc and nothing else.
That matters more than it sounds. Plenty of scanning setups key off CVE feeds, and a High-severity advisory with no CVE identifier is invisible to them. What does catch it is version-range tooling reading the GitHub Advisory Database directly — which is what Dependabot does, and one more argument for having dependency updates arrive as reviewable PRs rather than as a quarterly audit. If Dependabot PR volume is what stopped you from turning it on, grouped updates fix that specific complaint.
The inverse problem is also live right now: security inboxes are full of confident, fabricated vulnerability reports that cost maintainers real hours, which I dug into in the fake CVE reports flooding SQLite. GHSA-w3hf-fcg5-p4cc is the opposite of that — a specific, reproducible, credited report with a patch attached the same day. The signal-to-noise problem in vulnerability reporting is not that reports are too rigorous.
What to take back to your own code
You probably do not run Datasette. You almost certainly ship something that takes a column name, a sort field, or a table name from user input — every “sort by”, every faceted filter, every generic admin table view does.
Three things to check today:
- Find every identifier that comes from a request. Sort fields, filter columns, faceting, dynamic table names in multi-tenant code. Grep for string formatting near
ORDER BY,GROUP BY, andWHERE. - Allow-list, do not escape. The strongest form is to validate the identifier against the table’s actual column list and reject anything not in it. Quoting is the floor; an allow-list means a malformed identifier never reaches the SQL builder.
- Ask where your permission check runs relative to SQL composition. If the answer is “the check is on the route and the SQL is built afterwards,” you have the same structural gap Datasette just closed, whatever your quoting looks like.
The patch is one command. The habit is the part worth keeping.
FAQ
Which Datasette versions are affected by the SQL injection in GHSA-w3hf-fcg5-p4cc?
Two ranges are affected: everything below 0.65.3 on the stable branch, and everything from 1.0a0 up to but not including 1.0a38 on the alpha branch. The patched releases are 0.65.3 and 1.0a38, both published on 6 August 2026. If you are on any 1.0 alpha older than a38, or any stable release at all before 0.65.3, you are in scope.
Does disabling the execute-sql permission protect me?
No. The advisory is explicit that disabling execute-sql alone provides no protection. That permission governs the arbitrary-SQL box, and this bug never goes through it — the injection travels through ordinary table filter parameters in the query string, which are available to anyone who can view a table page. Turning off execute-sql is still good hygiene for a mixed-sensitivity database, but it does not close this hole.
Do I need to worry if all of my Datasette tables are public?
The permission bypass has nothing to take from you if there is nothing restricted in the database, since the vulnerability reads data the attacker could already reach. That said, upgrading is a one-line change and the fix is a general identifier-quoting hardening rather than a narrow patch, so there is no reason to stay behind. Treat it as low urgency, not as no action.
Is there a CVE for this Datasette vulnerability?
Not at the time of publication. The issue is tracked as GitHub Security Advisory GHSA-w3hf-fcg5-p4cc with a CVSS v3.1 base score of 7.5 (High), and no CVE identifier has been assigned. Scanners that key off CVE feeds alone will therefore stay silent on it, which is exactly why version-based dependency tooling matters more than vulnerability-database lookups here.
What is the workaround if I cannot upgrade immediately?
Split the database. The advisory’s guidance is to avoid mixing tables that untrusted users can reach with restricted tables in the same database file, so move the sensitive tables into a separate SQLite file that the untrusted audience has no permission to open. That is a stronger boundary than table-level permissions anyway, and it is worth keeping after you patch.
Why did parameterised queries not prevent this SQL injection?
Because parameter binding only covers values, never identifiers. You can bind the 5 in WHERE price > 5, but SQLite has no placeholder for the word price itself, so a column name arriving from a query string has to be escaped by the application. Datasette’s filter templates interpolated those identifiers without quoting them, and the fix was to quote table and column identifiers before they reach the generated SQL.
Sources
- GitHub Security Advisory, SQL Injection via unescaped column names in table filters (GHSA-w3hf-fcg5-p4cc), 6 August 2026 — affected ranges, CVSS 7.5 vector, the identifier-quoting fix, and the workaround. Reported by geo-chen.
- Simon Willison, datasette 1.0a38, 6 August 2026 — the release note, and the
execute-sqlguidance discussed above. - Datasette documentation, JSON API — table filter parameters — the
?column__operator=valueconvention the injection travels through.
Related Articles

Web Engineering
How to Make Your Site Agent-Readable: 4 Layers, One Worker
Make your site agent-readable in four layers — readable, discoverable, callable, payable. Three are build-time files; only /mcp needs a Worker.

Web Engineering
React Server Components in 2026: The Mental Model, the use client Boundary & When Not to Use Them
React Server Components in 2026: the mental model that finally clicks, the use client boundary rules, and when NOT to use them — with the Web Vitals payoff.

Web Engineering
Streaming HTML Out of Order Without JavaScript (2026)
Streaming HTML out of order without JavaScript: how Declarative Partial Updates and Declarative Shadow DOM reorder content natively in Chrome 148.
Keep reading
Get new posts on AI, Claude Code & LLMs
New deep-dives on AI engineering, Claude Code, and developer tooling — follow along however you prefer.
About the Author
Software engineer writing about AI, Claude Code, LLMs, OpenAI, Anthropic, and developer tooling. 5+ years building production systems at Expedia Group, Tekion, and BYJU'S.