docs: document NL2SQL read-only enforcement and its limits

The page claimed read-only mode blocked "multi-statement queries containing
semicolons" and said nothing about CTEs, EXPLAIN ANALYZE, or the fact that a
SELECT can still reach the database server's filesystem. Both gaps matter now
that those routes are enforced.

- Replace the semicolon sentence with a table of every indirect write route
  blocked in read-only mode: multi-statement, writable CTEs (including
  AS MATERIALIZED), a write after a CTE, EXPLAIN ANALYZE, INTO OUTFILE,
  server-filesystem functions, and unparseable WITH statements.
- Explain that analysis masks literals and comments, so `SELECT 'DROP TABLE
  users'` is allowed while `EXPLAIN /*x*/ ANALYZE DELETE ...` is blocked, and
  that a semicolon inside a literal no longer splits statements.
- Document the new SET TRANSACTION READ ONLY backstop and which backends
  fall back without it.
- Add a warning that these checks are defence in depth and a least-privileged
  read-only database role is the only complete control.

Every example in the new table was verified against the implementation.
Applied to en/ar/ko/pt-BR under docs/edge; the ko and pt-BR pages carry the
warning inline as they have no Hardening Recommendations section.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rip&Tear
2026-07-31 08:47:04 +08:00
parent 7f6367a21c
commit 338662201f
4 changed files with 76 additions and 4 deletions

View File

@@ -29,6 +29,10 @@ If you route untrusted input to agents using this tool, treat it as a high-risk
## Hardening Recommendations
<Warning>
The built-in read-only checks are defence in depth, not a complete boundary. They inspect the statement text, and SQL is dialect-specific: a statement beginning with `SELECT` can still reach the database server's filesystem (`SELECT ... INTO OUTFILE`, `pg_read_file()`) or call a side-effecting function. The known sinks are blocked explicitly, but the only complete control is the privileges you grant in `db_uri`. **Point the tool at a least-privileged, read-only database role.**
</Warning>
Use all of the following in production:
- Use a read-only database user whenever possible
@@ -49,7 +53,21 @@ Use all of the following in production:
Any attempt to execute a write operation (`INSERT`, `UPDATE`, `DELETE`, `DROP`, `CREATE`, `ALTER`, `TRUNCATE`, etc.) will raise an error unless DML is explicitly enabled.
Multi-statement queries containing semicolons (e.g. `SELECT 1; DROP TABLE users`) are also blocked in read-only mode to prevent injection attacks.
Read-only mode also blocks the indirect routes to a write:
| Blocked in read-only mode | Example |
| --- | --- |
| Multi-statement queries | `SELECT 1; DROP TABLE users` |
| Writable CTEs, including the materialised spelling | `WITH d AS MATERIALIZED (DELETE FROM users RETURNING *) SELECT * FROM d` |
| A write following a CTE | `WITH d AS (SELECT 1) DELETE FROM users` |
| `EXPLAIN ANALYZE`, which executes its argument | `EXPLAIN ANALYZE DELETE FROM users` |
| Writes to the database server's filesystem | `SELECT * FROM users INTO OUTFILE '/var/www/shell.php'` |
| Functions reaching the server's filesystem or opening a new connection | `SELECT pg_read_file('/etc/passwd')`, `dblink_exec(...)` |
| A `WITH` statement that cannot be parsed as read-only | `WITH d AS DELETE FROM users` |
Statements are analysed with string literals and comments masked out, so a keyword hidden in a literal is not mistaken for a command (`SELECT 'DROP TABLE users'` is allowed) and a comment placed between keywords does not hide one (`EXPLAIN /*x*/ ANALYZE DELETE ...` is blocked). A semicolon inside a string literal does not count as a statement separator, so `SELECT ';'` is a single valid statement.
In read-only mode the tool additionally marks the transaction `SET TRANSACTION READ ONLY`, so PostgreSQL and MySQL reject writes at the database regardless of how the statement was spelled. Backends without that syntax (SQLite, SQL Server, Snowflake) log a debug message and fall back to statement validation alone — one more reason to rely on a read-only role rather than on parsing.
### Enabling Write Operations