Vulnerability DatabaseGHSA-r77g-5v43-wh54

GHSA-r77g-5v43-wh54: 
Python vulnerability analysis and mitigation

Summary

  • Discoverer (draft credit): Artur Chlebicki arturichlebicki@gmail.com
  • Class: Authorization bypass / security-control bypass (entitlement check evasion). CWE-284 (Improper Access Control); CWE-909 (Evacuation of Sensitive Information by the scope checker) as secondary. Arguably CWE-89-shaped (the guard exists precisely to stop attacker SQL it fails to stop).
  • Affected: 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.
  • Component: 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.
  • Databases affected: PostgreSQL (core 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).

Details

  • Discoverer (draft credit): Artur Chlebicki arturichlebicki@gmail.com
  • Class: Authorization bypass / security-control bypass (entitlement check evasion). CWE-284 (Improper Access Control); CWE-909 (Evacuation of Sensitive Information by the scope checker) as secondary. Arguably CWE-89-shaped (the guard exists precisely to stop attacker SQL it fails to stop).
  • Affected: 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.
  • Component: 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.
  • Databases affected: PostgreSQL (core 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).

Impact (boundary crossed)

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 → sink trace (all on tag v1.14.0)

…(full source→sink trace available on request)

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.

Proof of concept

Source — attacker-controlled sql string supplied by an MCP client to the run_query tool:

  1. 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 result
    where 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 orders
  2. src/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.
  3. 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:
  4. 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).
  5. 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)).
  6. On PostgreSQL, 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).

Impact

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

Related Python vulnerabilities:

CVE ID

Severity

Score

Technologies

Component name

CISA KEV exploit

Has fix

Published date

GHSA-r77g-5v43-wh54MEDIUM6.5
  • Python logoPython
  • schemagate
NoYesOct 29, 2026
GHSA-q69g-f4f5-v8m7MEDIUM6.2
  • Python logoPython
  • onnx
NoYesOct 28, 2026
GHSA-r4j5-9xpp-3985MEDIUM5.3
  • Python logoPython
  • tornado
NoYesOct 28, 2026
GHSA-pcg2-g6cj-q92cMEDIUM5.3
  • Python logoPython
  • tornado
NoYesOct 28, 2026
GHSA-pw9p-v7r6-gx4pLOW2.1
  • Python logoPython
  • tornado
NoYesOct 28, 2026

Free Vulnerability Assessment

Benchmark your Cloud Security Posture

Evaluate your cloud security practices across 9 security domains to benchmark your risk level and identify gaps in your defenses.

Request assessment

Additional Wiz resources

Get a personalized demo

Ready to see Wiz in action?

"Best User Experience I have ever seen, provides full visibility to cloud workloads."
David EstlickCISO
"Wiz provides a single pane of glass to see what is going on in our cloud environments."
Adam FletcherChief Security Officer
"We know that if Wiz identifies something as critical, it actually is."
Greg PoniatowskiHead of Threat and Vulnerability Management