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 ...
Affected versions
| Package | Affected | Fixed in |
|---|---|---|
| package mcp-contextforge-gateway PyPI | < v1.0.7 | v1.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
More IBM advisories
All IBM| Date | Advisory | Severity | Fixed in |
|---|---|---|---|
| Aug 25 | Scoped admin OAuth DCR endpoints ignore token_teams restrictions and expose global registered clients | Medium6.4 | v1.0.7 |
| Aug 25 | Server-Side Request Forgery (SSRF) in url_to_markdown_server via Unvalidated URL Fetching | Critical9.1 | v1.0.7 |
| Aug 25 | Server-Side Request Forgery (SSRF) in mcp-rss-search via Unvalidated URL Fetching | High8.6 | v1.0.7 |
| Aug 25 | Session auth-context reuse ignores JWT exp — expired JWT replayable within session_auth_reuse_ttl | Medium | v1.0.5 |
| Aug 25 | Cross-tenant read of plugin-binding policies (BOLA) in ContextForge MCP Gateway | High | v1.0.5 |
| Aug 25 | Default JWT Secret in Docker Compose Enables Forged Platform-Admin Tokens | Critical9.8 | v1.0.7 |