إرسال #872772: facebook-ads-mcp-server gomarble-ai Latest Server-Side Request Forgeryالمعلومات

عنوانfacebook-ads-mcp-server gomarble-ai Latest Server-Side Request Forgery
الوصفSummary The fetch_pagination_url tool in facebook-ads-mcp-server is vulnerable to Server-Side Request Forgery (SSRF). The user-supplied url parameter is passed directly to Python's requests.get() without any validation of the destination host, IP range, or protocol. This allows an attacker to make the MCP server issue arbitrary HTTP requests to internal network services, cloud metadata endpoints, or any other host reachable from the server process — and retrieve the parsed response content when the target returns valid JSON. Detail facebook-ads-mcp-server provides AI programming assistants (such as Claude, Gemini, and Cursor) with programmatic access to the Meta (Facebook) Ads API via the Model Context Protocol. The fetch_pagination_url tool is intended to follow pagination links returned by insights API calls (response['paging']['next'] / response['paging']['previous']). However, it accepts a complete URL string from the caller and fetches it directly, bypassing the shared helper that constrains all other outbound requests to the Facebook Graph API. The tool is registered via FastMCP with an unconstrained url: str parameter — any string value is accepted, including internal URLs: Version: 0.1.0 File: server.py @mcp.tool() def fetch_pagination_url(url: str) -> Dict: """Fetch data from a Facebook Graph API pagination URL Use this to get the next/previous page of results from an insights API call. Args: url: The complete pagination URL (e.g., from response['paging']['next'] or response['paging']['previous']). It includes the necessary token and parameters. Returns: The dictionary containing the next/previous page of results. Example: ```python # Assuming 'initial_results' is the dict from a previous insights call if "paging" in initial_results and "next" in initial_results["paging"]: next_page_data = fetch_pagination_url(url=initial_results["paging"]["next"]) if "paging" in initial_results and "previous" in initial_results["paging"]: prev_page_data = fetch_pagination_url(url=initial_results["paging"]["previous"]) ``` """ # This function takes a full URL which already includes the access token, # so we don't use the _make_graph_api_call helper here. response = requests.get(url) response.raise_for_status() return response.json() The handler passes the user-supplied string directly to requests.get() with no intermediate validation: Version: 0.1.0 File: server.py # This function takes a full URL which already includes the access token, # so we don't use the _make_graph_api_call helper here. response = requests.get(url) response.raise_for_status() return response.json() Unlike every other outbound HTTP call in this server, fetch_pagination_url does not use _make_graph_api_call(), which always receives a URL constructed from a hardcoded base: Version: 0.1.0 File: server.py # --- Constants --- FB_API_VERSION = "v22.0" FB_GRAPH_URL = f"https://graph.facebook.com/{FB_API_VERSION}" Version: 0.1.0 File: server.py def _make_graph_api_call(url: str, params: Dict[str, Any]) -> Dict: """Makes a GET request to the Facebook Graph API and handles the response.""" try: response = requests.get(url, params=params) response.raise_for_status() # Raises HTTPError for bad responses (4xx or 5xx) return response.json() except requests.exceptions.RequestException as e: # Log the error and re-raise or handle more gracefully print(f"Error making Graph API call to {url} with params {params}: {e}") # Depending on desired behavior, you might want to raise a custom exception # or return a specific error structure. Re-raising keeps the current behavior. raise All other tools build their destination from FB_GRAPH_URL plus a user-controlled ID or edge name. For example, _fetch_node() always targets graph.facebook.com: Version: 0.1.0 File: server.py def _fetch_node(node_id: str, **kwargs) -> Dict: """Helper to fetch a single object (node) by its ID.""" access_token = _get_fb_access_token() url = f"{FB_GRAPH_URL}/{node_id}" params = _prepare_params({'access_token': access_token}, **kwargs) return _make_graph_api_call(url, params) Only fetch_pagination_url grants the caller full control over the request destination. The tool is also advertised in the server manifest with no security constraints: Version: 0.1.0 File: manifest.json { "name": "fetch_pagination_url", "description": "Fetches data from a pagination URL" }, There is no validation at any point in this call chain: No allowlist of permitted domains (e.g. graph.facebook.com) No blocklist of private/internal IP ranges (127.x, 10.x, 172.16–31.x, 192.168.x, 169.254.x) No restriction on URL scheme (only http:/https: should be permitted) No limit on HTTP redirects (requests.get() follows redirects by default, enabling redirect-based bypass into internal space) No timeout configured on the outbound request The complete parsed JSON response is returned to the MCP client via return response.json(). For targets that return JSON (e.g. cloud instance metadata endpoints, internal REST APIs), this constitutes direct data exfiltration to the Agent. Even for non-JSON targets, the server still issues the request; a side-channel listener can confirm SSRF regardless of whether the tool call succeeds. Clarified: This attack does not require OS-level shell access. The practically feasible triggering path is: an attacker performs a prompt injection into an AI agent integrated with facebook-ads-mcp-server, instructing the agent to invoke fetch_pagination_url with a target internal URL (often framed as "fetch the next page of results from this URL"). The agent, having no awareness of SSRF risks, will comply. No direct MCP client access is required beyond the ability to influence the agent's prompts (e.g., via crafted file content, web pages, emails, or ad copy the agent is asked to process). Note on other tools: Tools such as get_ad_insights, get_campaign_insights, and get_adaccount_insights reference fetch_pagination_url only inside docstring examples (e.g. server.py lines 364–368); they do not invoke it at runtime. The sole SSRF entry point is the exposed fetch_pagination_url MCP tool. Proof of Concept Using MCP Inspector Prerequisites Start the MCP server via Inspector: cd facebook-ads-mcp-server pip install -r requirements.txt npx @modelcontextprotocol/inspector python server.py --fb-token YOUR_META_ACCESS_TOKEN This launches the MCP Inspector web UI In a separate terminal, start a local HTTP listener to capture SSRF requests: python -m http.server 8000 Steps Open the Inspector URL in a browser and click Connect. Go to the Tools tab, click List Tools, and select fetch_pagination_url. Invoke fetch_pagination_url with the following parameters: http://127.0.0.1:8000/ssrf-test Observe the outgoing MCP request: { "method": "tools/call", "params": { "name": "fetch_pagination_url", "arguments": { "url": "http://127.0.0.1:8000/ssrf-test" }, "_meta": { "progressToken": 0 } } } Observe the listener output — the MCP server process (not the browser) issues the request
المصدر⚠️ https://github.com/gomarble-ai/facebook-ads-mcp-server/issues/29
المستخدم
 TianyuLi (UID 99360)
ارسال29/06/2026 03:11 AM (2 أشهر منذ)
الاعتدال16/08/2026 08:08 AM (2 months later)
الحالةتمت الموافقة
إدخال VulDB391113 [gomarble-ai facebook-ads-mcp-server 0.1.0 server.py fetch_pagination_url تجاوز الصلاحيات]
النقاط20

Do you know our Splunk app?

Download it now for free!