Skip to content
IBMGHSA-9qxh-fpx5-vxvw

Parse-gap bypasses in mcp-context-forge: SQL Sanitizer (privesc/DROP/mass-DELETE) and resource_filter SSRF

High8.8Published Aug 25, 2026

### Summary Two shipping policy plugins — `sql_sanitizer` and `resource_filter` — decide over a **string** (flat regex / `urlparse().netloc`) while the downstream consumer parses the same string with a **real grammar** (a SQL engine; an RFC-3986/WHATWG URL parser). The two parses disagree, so a plugin **allows** an argument the downstream then executes as the exact forbidden operation the plugin exists to block (a monitor-vs-executor parse gap). Impact: the SQL sanitizer can be bypassed to run privilege-escalating `UPDATE`, `DROP TABLE`, and unscoped mass `DELETE`; the resource filter can be bypassed for SSRF to a blocked domain. Confirmed on `main` @ `7b17f124bec1c46a94ab0d619c9727493236dffa` (2026-07-16). ### Details **SQL Sanitizer** — `plugins/sql_sanitizer/sql_sanitizer.py`. There is no real SQL parser (`import re` only, line ~21). `_find_issues()` (line ~138) strips comments with `_BLOCK_COMMENT_RE = re.compile(r"/\*.*?\*/", re.DOTALL)` (line ~55, applied line ~119), keyword-blocklists `DROP/TRUNCATE/…` via regex, and flags `DELETE`/`UPDATE` "without WHERE" via `_WHERE_RE = re.compile(r"\bWHERE\b", re.IGNORECASE)` (line ~58; checks at lines ~158 and ~162). Because these are ...

GitHub advisory

Affected versions

PackageAffectedFixed in
package mcp-contextforge-gateway
PyPI
< v1.0.7v1.0.7
Details and references

### Summary Two shipping policy plugins — `sql_sanitizer` and `resource_filter` — decide over a **string** (flat regex / `urlparse().netloc`) while the downstream consumer parses the same string with a **real grammar** (a SQL engine; an RFC-3986/WHATWG URL parser). The two parses disagree, so a plugin **allows** an argument the downstream then executes as the exact forbidden operation the plugin exists to block (a monitor-vs-executor parse gap). Impact: the SQL sanitizer can be bypassed to run privilege-escalating `UPDATE`, `DROP TABLE`, and unscoped mass `DELETE`; the resource filter can be bypassed for SSRF to a blocked domain. Confirmed on `main` @ `7b17f124bec1c46a94ab0d619c9727493236dffa` (2026-07-16). ### Details **SQL Sanitizer** — `plugins/sql_sanitizer/sql_sanitizer.py`. There is no real SQL parser (`import re` only, line ~21). `_find_issues()` (line ~138) strips comments with `_BLOCK_COMMENT_RE = re.compile(r"/\*.*?\*/", re.DOTALL)` (line ~55, applied line ~119), keyword-blocklists `DROP/TRUNCATE/…` via regex, and flags `DELETE`/`UPDATE` "without WHERE" via `_WHERE_RE = re.compile(r"\bWHERE\b", re.IGNORECASE)` (line ~58; checks at lines ~158 and ~162). Because these are flat-text operations over the raw query string, several parse divergences with a real SQL engine slip past the checks (see PoC). **resource_filter** — `plugins/resource_filter/resource_filter.py`. The domain filter computes `parsed = urlparse(payload.uri)` then `domain_lower = parsed.netloc.lower()` (line ~112) and blocks by equality / `endswith("." + d)` (line ~114). `netloc` **includes any `userinfo@`**, so it differs from the host a URL library actually connects to (`urlparse(uri).hostname`), enabling SSRF (see PoC). ### PoC **SQL Sanitizer** (shipping defaults `block_update_without_where`, `block_delete_without_where`, `strip_comments`). Each returns *no issue (allow)*; a SQL engine executes the destructive operation (verified with the real `_find_issues` and stdlib `sqlite3`): 1. **Privilege escalation** — `WHERE` inside a string literal satisfies the "UPDATE without WHERE" check: `UPDATE users SET is_admin=1, note='flagged WHERE approved'` 2. **DDL bypass** — `DROP` hidden between string literals is removed by the DOTALL comment stripper before the blocklist runs: `SELECT '/*' AS x; DROP TABLE users; SELECT '*/' AS y` 3. **Unscoped DELETE** — statement-scope confusion (the `WHERE` belongs to the trailing `SELECT`): `DELETE FROM users; SELECT 1 WHERE 1=1` **resource_filter** (`blocked_domains=["evil.com"]`): `http://good.com@evil.com/x` → `netloc = "good.com@evil.com"`, which neither equals `evil.com` nor ends with `.evil.com` → **not blocked**. The true host — per Python `urlparse(...).hostname` **and** Node `new URL(...).hostname` — is `evil.com` → SSRF past the filter. (Engine-specific variant to review: MySQL/MariaDB executable comments `/*! … */`, executed by the engine but treated as a comment by the stripper/parser; not tested against a live MySQL.) ### Impact The SQL sanitizer is deployed to stop dangerous SQL reaching a database MCP server → the bypasses yield full-table privilege escalation, table drops, and mass deletion (integrity/confidentiality/availability of the DB). The resource filter is deployed to block egress to disallowed hosts → the bypass yields SSRF to a blocked domain. Any deployment relying on these plugins as a security control is affected. Root cause is a monitor-vs-executor interpretation conflict: the plugin's string/regex view diverges from the executor's grammar. Remediation: decide over a real parse — `sqlglot` typed statements for SQL (per-statement kind + a genuine `WHERE` node, not a keyword inside a literal), and `urlparse(uri).hostname` (+ IDN/alt-IP normalization) for URLs — binding the policy to the same canonical parse the downstream uses. Reproduction scripts available; we propose a 90-day coordinated- disclosure embargo (part of a

CVSS 3.1
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
Severity from
GitHub (reviewed advisory)
Weakness
CWE-89, CWE-436, CWE-918

More IBM advisories

All IBM

Critical advisories by email

Wednesdays: the week’s critical and high advisories in the AI and data stack, with the fixed versions. Only in weeks that have some.

Double opt-in. Unsubscribe any time.