Summary

A Server-Side Request Forgery (SSRF) vulnerability was discovered in the webpage-to-markdown conversion feature of the markdownify-mcp server. This vulnerability allows an attacker to bypass private IP restrictions through hostname-based bypass and HTTP redirect chains, enabling access to internal network services. The impact includes potential exposure of sensitive internal administrative endpoints and credentials.


Details

Vulnerability Overview

The markdownify-mcp server provides functionality to convert web pages to markdown format by fetching user-supplied URLs. It performs an HTTP request from the server to the provided URL and converts the response (HTML) to markdown format.

The issue arises because the feature performs these HTTP requests without properly validating the user-supplied URL. In particular, two critical flaws exist:

  1. The is_ip_private() function only validates IP address strings but does not resolve hostnames to their actual IP addresses
  2. The fetch() function automatically follows HTTP redirects without re-validating the redirected destination URLs

This allows attackers to access internal services through hostname bypass (e.g., using "localhost" instead of "127.0.0.1") or by chaining HTTP redirections from public or localhost URLs to any internal addresses (127.0.0.1, 192.168.x.x, 10.x.x.x, 172.16-31.x.x).

Root Cause

The fetch() function is called without proper URL validation or hostname resolution, and redirects are automatically followed without re-validation, which enables attackers to access internal services on the private network.

Taint Flow

src/server.ts Lines 41-42

const { name, arguments: args } = request.params;
const validatedArgs = RequestPayloadSchema.parse(args);

src/server.ts Lines 55-62

const parsedUrl = new URL(validatedArgs.url);

if (!["http:", "https:"].includes(parsedUrl.protocol)) {
  throw new Error("Only http: and https: schemes are allowed.");
}

if (is_ip_private(parsedUrl.hostname)) {
  throw new Error(`Fetching ${validatedArgs.url} is potentially dangerous, aborting.`);
}

The validation only checks the hostname string without DNS resolution: