
Cloud Vulnerability DB
A community-led vulnerabilities database
schemagate 1.14.0 (PyPI; tag v1.14.0, commit
0a1c23ae0ec9038cfe97260154d973311a864839, released 2026-10-27 — newest release at time of
audit). All 1.x at least back to the introduction of run_query; the sqlglot hardening added
after GHSA-qhcw/GHSA-2mmx/GHSA-c258 does not cover this sink.schemagate.mcp_server.run_query (and every tool that routes through it:
semantic_query, answer, deep_answer), gates schemagate.answer.check_read_only and
schemagate.mcp_server._check_scope, sink schemagate.answer.run_sql.pg_catalog XML functions, EXECUTE granted to PUBLIC,
no extension needed) and Oracle (DBMS_XMLGEN, EXECUTE granted to PUBLIC by default).
MySQL/MSSQL/SQLite are not affected by this specific vector (no dynamic-SQL function callable
inside a plain SELECT there).schemagate 1.14.0 (PyPI; tag v1.14.0, commit
0a1c23ae0ec9038cfe97260154d973311a864839, released 2026-10-27 — newest release at time of
audit). All 1.x at least back to the introduction of run_query; the sqlglot hardening added
after GHSA-qhcw/GHSA-2mmx/GHSA-c258 does not cover this sink.schemagate.mcp_server.run_query (and every tool that routes through it:
semantic_query, answer, deep_answer), gates schemagate.answer.check_read_only and
schemagate.mcp_server._check_scope, sink schemagate.answer.run_sql.pg_catalog XML functions, EXECUTE granted to PUBLIC,
no extension needed) and Oracle (DBMS_XMLGEN, EXECUTE granted to PUBLIC by default).
MySQL/MSSQL/SQLite are not affected by this specific vector (no dynamic-SQL function callable
inside a plain SELECT there).schemagate's whole product claim is: "a table the caller may not read never enters the prompt,
and run_query refuses any table this caller may not see." A caller with no roles at all
(any authenticated principal in the OAuth-secured HTTP deployment the project recommends;
anyone who can reach the port in the no-OAuth HTTP deployment, which main() permits with only a
log warning) submits a SELECT whose restricted table is named inside a string literal passed to
a database function that executes it. Both gates pass, the statement is executed verbatim on the
service's database connection, and the restricted table's rows come back serialized as XML text in
the tool result. This defeats table-level restrictions (Catalog.restrict,
SCHEMAGATE_RESTRICT_FROM_GRANTS, row-policy withholding) and column-level restrictions
(restrict_column), and is not covered by the project's documented "client-asserted roles are not
a lock" caveat; the bypass works even when identity is OAuth-verified and roles come from the
token/directory.
Severity: CVSS 6.5 Medium in the OAuth-secured deployment
(CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N); 7.5 High in the plain HTTP deployment the
code permits (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N): full read of every restricted
table's data (C:H), confidentiality-only.
Please publish this as a GitHub Security Advisory and request a CVE (GitHub can assign one as a CNA). Please credit Artur Chlebicki (GitHub: @Artur12555) as the reporter/discoverer. I'm happy to verify a fix or provide any further detail. Thank you! Credit: @Artur12555.
Source — attacker-controlled sql string supplied by an MCP client to the run_query tool:
src/schemagate/mcp_server.py:995 — def run_query(sql: SqlArg, principal=None, roles=None, max_rows=...).
The MCP transport (stdio desktop client, or streamable-http at SCHEMAGATE_MCP_PORT,
mcp_server.py:1546-1555) delivers the tool arguments; sql is fully attacker-controlled text
(≤ MAX_SQL_CHARS = 20000, mcp_server.py:272). Payload (PostgreSQL):SELECT query_to_xml('SELECT * FROM hr_compensation', true, true, '') AS resultwhere hr_compensation is restricted to payroll (config {"restrict": {...}} via
SCHEMAGATE_CATALOG_CONFIG, or grants/RLS reflection) and the caller has no roles. Variant that
also works on Oracle (Oracle needs a FROM, so the attacker selects from any visible table):SELECT DBMS_XMLGEN.GETXML('SELECT * FROM hr_compensation') AS r FROM orderssrc/schemagate/mcp_server.py:1026 — sql = check_read_only(sql); passes:src/schemagate/answer.py:200-205 — comments stripped, then every single-quoted literal is
blanked out (scan = re.sub(r"'(?:[^']|'')*'", "''", bare)); the design comment at
answer.py:202-204 states the assumption: "A string literal can legitimately contain any
word at all". That assumption is false on PostgreSQL/Oracle, where a literal can contain a
whole query that the database itself executes.answer.py:207 no ; outside literals → single statement; answer.py:209 starts with
SELECT; answer.py:211-213 _FORBIDDEN word scan sees only the literal-stripped scan —
query_to_xml, table_to_xml, dbms_xmlgen are not in the denylist and the payload words
were blanked anyway.answer.py:214 — returns the original sql with the literal intact.src/schemagate/mcp_server.py:1031 — denied = _check_scope(cat, sql, who); returns None
(allowed):mcp_server.py:608-616 builds visible/hidden name sets (hr_compensation ∈ hidden for this
caller).mcp_server.py:634-638 — _parsed_refs(sql) (sqlglot, added by the GHSA-c258 hardening)
parses the statement successfully (a function call with a String argument is a normal
expression) and finds zero exp.Table nodes (mcp_server.py:578-585): the restricted
name is a String literal argument, not a table reference. Verified live: returns
([], set(), False).mcp_server.py:639 — _referenced_tables(sql) regex reader: _table_body
(mcp_server.py:489-508) empties the literal (_STRING.sub(one, ...), the FROM inside
it disappears; the SQLite table-position exception at mcp_server.py:505 does not apply — the
literal follows (, not FROM/JOIN/,/.), so _REFERENCED (mcp_server.py:463) finds
no FROM/JOIN → [].mcp_server.py:649-654 — identifier backstop _identifiers(sql) runs on _sql_body(sql)
(mcp_server.py:474-478, literals emptied) → {select, query_to_xml, true, as, result} —
hr_compensation never appears; intersection with hidden names is empty.mcp_server.py:667-683 — column rules see no referenced tables → nothing withheld applies.mcp_server.py:684 — returns None → allowed.
Sink — the statement is executed verbatim on the service database connection:src/schemagate/mcp_server.py:1039 — cols, rows = run_sql(_engine(), sql, ...); engine built
from SCHEMAGATE_DATABASE_URL at mcp_server.py:436-447 — the service's credentials, which
in the schemagate model do have SELECT on the restricted table (per-caller entitlement is
enforced by schemagate, not by the DB).src/schemagate/answer.py:362 — run_sql re-runs check_read_only (passes again), then
src/schemagate/answer.py:367 — result = conn.execute(text(sql)).query_to_xml / table_to_xml are pg_catalog functions (EXECUTE to PUBLIC, no
extension, no superuser) that execute their text argument server-side and emit the result rows
as XML; on Oracle, DBMS_XMLGEN.GETXML (EXECUTE to PUBLIC by default) does the same. The
restricted table's rows are returned to the MCP client as the tool result (mcp_server.py:1053-1061).schemagate's whole product claim is: "a table the caller may not read never enters the prompt,
and run_query refuses any table this caller may not see." A caller with no roles at all
(any authenticated principal in the OAuth-secured HTTP deployment the project recommends;
anyone who can reach the port in the no-OAuth HTTP deployment, which main() permits with only a
log warning) submits a SELECT whose restricted table is named inside a string literal passed to
a database function that executes it. Both gates pass, the statement is executed verbatim on the
service's database connection, and the restricted table's rows come back serialized as XML text in
the tool result. This defeats table-level restrictions (Catalog.restrict,
SCHEMAGATE_RESTRICT_FROM_GRANTS, row-policy withholding) and column-level restrictions
(restrict_column), and is not covered by the project's documented "client-asserted roles are not
a lock" caveat; the bypass works even when identity is OAuth-verified and roles come from the
token/directory.
Severity: CVSS 6.5 Medium in the OAuth-secured deployment
(CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N); 7.5 High in the plain HTTP deployment the
code permits (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N): full read of every restricted
table's data (C:H), confidentiality-only.
Source: NVD
Free Vulnerability Assessment
Evaluate your cloud security practices across 9 security domains to benchmark your risk level and identify gaps in your defenses.
Get a personalized demo
"Best User Experience I have ever seen, provides full visibility to cloud workloads."
"Wiz provides a single pane of glass to see what is going on in our cloud environments."
"We know that if Wiz identifies something as critical, it actually is."