Server-Side Request Forgery (SSRF) in url_to_markdown_server via Unvalidated URL Fetching
Critical9.1Published Aug 25, 2026
## Summary The url_to_markdown_server module, a production MCP server distributed as part of IBM/mcp-context-forge, contains a critical Server-Side Request Forgery (SSRF) vulnerability. The fetch_url_content() method in UrlToMarkdownConverter directly fetches attacker-controlled URLs via httpx without any SSRF protections. An attacker who can invoke the convert_url MCP tool can force the server to make HTTP requests to arbitrary internal network destinations, including cloud metadata services (169.254.169.254), internal APIs, databases, and other private network resources. This vulnerability is exploitable in the default configuration and requires no elevated privileges. ## Details ## Root Cause The vulnerability exists because the fetch_url_content() method in the UrlToMarkdownConverter class performs no validation on the target URL before making an outbound HTTP request. ## Trust Boundary Failure The MCP protocol serves as a trust boundary between the caller and the server. The convert_url tool (line 710) accepts a url parameter from across this trust boundary and passes it directly to fetch_url_content() (line 731), which issues an HTTP GET request via httpx.AsyncClient.get(ur...
Affected versions
| Package | Affected | Fixed in |
|---|---|---|
| mcp-contextforge-gateway PyPI | < v1.0.7 | v1.0.7 |
Details and references
## Summary The url_to_markdown_server module, a production MCP server distributed as part of IBM/mcp-context-forge, contains a critical Server-Side Request Forgery (SSRF) vulnerability. The fetch_url_content() method in UrlToMarkdownConverter directly fetches attacker-controlled URLs via httpx without any SSRF protections. An attacker who can invoke the convert_url MCP tool can force the server to make HTTP requests to arbitrary internal network destinations, including cloud metadata services (169.254.169.254), internal APIs, databases, and other private network resources. This vulnerability is exploitable in the default configuration and requires no elevated privileges. ## Details ## Root Cause The vulnerability exists because the fetch_url_content() method in the UrlToMarkdownConverter class performs no validation on the target URL before making an outbound HTTP request. ## Trust Boundary Failure The MCP protocol serves as a trust boundary between the caller and the server. The convert_url tool (line 710) accepts a url parameter from across this trust boundary and passes it directly to fetch_url_content() (line 731), which issues an HTTP GET request via httpx.AsyncClient.get(url) (line 150) with no intermediate security checks. ## Source-to-Sink Chain 1. Entry Point (lines 709-724): convert_url MCP tool receives user-controlled url via MCP protocol. 2. Direct Pass-through (line 731): url forwarded to fetch_url_content() with no validation. 3. HTTP Client (lines 125-141): httpx.AsyncClient created with follow_redirects=True, amplifying SSRF risk. 4. Dangerous Sink (lines 143-150): session.get(url) issues HTTP GET to attacker-controlled URL with no IP validation, no allowlisting, and no DNS rebinding protection. ## Missing Security Controls No ipaddress checks for private IPs, loopback, link-local, or CGNAT. No DNS rebinding protection. No URL allowlisting. ## Contrast with Gateway The main mcpgateway has robust SSRF protection in SecurityValidator.validate_gateway_test_url() (validators.py line 1526). The url_to_markdown_server lacks all of these. Core vulnerable code path: ```python # mcp-servers/python/url_to_markdown_server/src/url_to_markdown_server/server_fastmcp.py:709-731 async def convert_url( url: str = Field(..., description="URL to retrieve and convert to markdown"), timeout: int = Field(DEFAULT_TIMEOUT, le=MAX_TIMEOUT), include_images: bool = Field(True), include_links: bool = Field(True), clean_content: bool = Field(True), extraction_method: str = Field("auto", pattern="^(auto|readability|raw)quot;), markdown_engine: str = Field("html2text", pattern="^(html2text|markdownify|beautifulsoup|basic)quot;), ) -> dict[str, Any]: conversion_id = str(uuid4()) try: fetch_result = await converter.fetch_url_content(url, timeout) ``` Entry point: the convert_url MCP tool receives a user-controlled url parameter from across the MCP trust boundary. No URL validation occurs before calling fetch_url_content(). ```python # mcp-servers/python/url_to_markdown_server/src/url_to_markdown_server/server_fastmcp.py:143-150 async def fetch_url_content(self, url: str, timeout: int = DEFAULT_TIMEOUT) -> dict[str, Any]: try: session = await self.get_session() logger.info(f"Fetching URL: {url}") response = await session.get(url, timeout=timeout) response.raise_for_status() ``` Dangerous sink: the attacker-controlled url is passed directly to session.get() with no IP validation, no private-IP blocking, no DNS rebinding protection, and no allowlist check. ## POC ## Preconditions 1. The url_to_markdown_server must be deployed and accessible via MCP protocol. 2. The attacker needs MCP tool invocation capability. 3. For cloud metadata attacks, the server must run in a cloud environment. ## Proof of Concept Step 1 (AWS Cloud Metadata): Call convert_url with url=http://169.254.169.254/latest/meta-data/ S
- CVSS 3.1
- CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N
- Severity from
- GitHub (reviewed advisory)
- Weakness
- CWE-918
More IBM advisories
All IBM| Date | Advisory | Severity | Fixed in |
|---|---|---|---|
| Aug 25 | Parse-gap bypasses in mcp-context-forge: SQL Sanitizer (privesc/DROP/mass-DELETE) and resource_filter SSRF | High8.8 | v1.0.7 |
| 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 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 |