meta_ads_mcp/core/api.py:136 — operator token appended to URL query parameters, exposed verbatim in Graph API error response request_url
request_params = params or {}
request_params["access_token"] = access_token
Unauthenticated HTTP POST /mcp → AuthInjectionMiddleware.dispatch():272 (no 401 returned) → tool handler invokes make_api_request() using META_ACCESS_TOKEN env fallback → request_params["access_token"]:136 (token in URL) → Graph API error path at api.py:263–269 returns request_url containing access_token=… in 200 OK JSON-RPC response.
Proof of Concept
Step 1 — POST /mcp with no auth headers: HTTP 200 OK with operator access_token in request_url — proves unauthenticated tool execution and operator credential leakage.
An unauthenticated attacker who can reach the MCP server's HTTP port (default 8080) can invoke any registered MCP tool as the operator, consuming the operator's Meta Ads API quota and performing read or write operations on connected Meta ad accounts. When any tool call triggers a Graph API error, the operator's META_ACCESS_TOKEN is returned verbatim in the request_url field of the 200 OK JSON-RPC response, enabling the attacker to exfiltrate the long-lived credential and subsequently access the Meta Graph API directly outside the MCP interface.
Remediation
In AuthInjectionMiddleware.dispatch() (http_auth_integration.py), return a 401 Unauthorized response when neither auth_token nor pipeboard_token is present, instead of falling through to call_next:
from starlette.responses import Response
if not auth_token and not pipeboard_token:
return Response(
content='{"error":"Unauthorized"}',
status_code=401,
media_type="application/json",
)
In make_api_request() (api.py), strip access_token from the request_url in error payloads, or transmit the token via an Authorization: Bearer header rather than a URL query parameter to prevent it from appearing in URLs, server logs, or error responses.