Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion meta_ads_mcp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from meta_ads_mcp.core.server import main

__version__ = "1.0.113"
__version__ = "1.0.114"

__all__ = [
'get_ad_accounts',
Expand Down
15 changes: 9 additions & 6 deletions meta_ads_mcp/core/http_auth_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,12 +256,15 @@ async def dispatch(self, request: Request, call_next):
auth_token = FastMCPAuthIntegration.extract_token_from_headers(dict(request.headers))
pipeboard_token = FastMCPAuthIntegration.extract_pipeboard_token_from_headers(dict(request.headers))

if not auth_token and not pipeboard_token:
# Reject unauthenticated requests. Without this, the request would fall
# through to tool handlers which transparently use the META_ACCESS_TOKEN
# env var, allowing any network-reachable caller to invoke tools and
# exfiltrate the operator's credential via Graph API error responses.
# See GHSA-9gw6-46qc-99vr.
if not auth_token:
# A request must carry a primary access-token credential: an
# Authorization: Bearer header, X-META-ACCESS-TOKEN, or
# X-PIPEBOARD-API-TOKEN (all resolved by extract_token_from_headers).
# X-Pipeboard-Token is only a supplementary service token for the
# duplication callback — on its own it establishes no request auth
# context, so it must not admit a request. Otherwise the request
# would fall through to tool handlers that use the META_ACCESS_TOKEN
# env var. See GHSA-9gw6-46qc-99vr.
logger.warning(
"HTTP Auth Middleware: rejecting request to %s — no Authorization "
"Bearer or X-PIPEBOARD-API-TOKEN header present", request.url.path,
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "meta-ads-mcp"
version = "1.0.113"
version = "1.0.114"
description = "Model Context Protocol (MCP) server for Meta Ads - Use Remote MCP at pipeboard.co for easiest setup"
readme = "README.md"
requires-python = ">=3.10"
Expand Down
2 changes: 1 addition & 1 deletion server.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
"name": "co.pipeboard/meta-ads-mcp",
"description": "Facebook / Meta Ads automation with AI: analyze performance, test creatives, optimize spend.",
"version": "1.0.113",
"version": "1.0.114",
"remotes": [
{
"type": "streamable-http",
Expand Down
32 changes: 32 additions & 0 deletions tests/test_http_auth_security.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,38 @@ def test_middleware_accepts_pipeboard_token():
assert resp.status_code == 200


def test_middleware_rejects_supplementary_token_alone():
"""X-Pipeboard-Token is a supplementary service token (duplication
callback) and must not, on its own, admit a request — without a primary
access-token credential the request must be rejected with 401."""
client = TestClient(_build_app())
resp = client.post(
"/mcp",
json={"jsonrpc": "2.0", "method": "tools/list", "id": 1},
headers={"X-Pipeboard-Token": "anything-not-validated"},
)
assert resp.status_code == 401
assert resp.headers.get("WWW-Authenticate") == "Bearer"
assert resp.json()["error"] == "Unauthorized"
assert "reached_handler" not in resp.text


def test_middleware_accepts_supplementary_token_with_primary_credential():
"""The legitimate flow forwards X-Pipeboard-Token alongside a primary
credential; the request must still be admitted."""
client = TestClient(_build_app())
resp = client.post(
"/mcp",
json={"jsonrpc": "2.0", "method": "tools/list", "id": 1},
headers={
"Authorization": "Bearer some-meta-token-value-xyz",
"X-Pipeboard-Token": "pb-supplementary-token",
},
)
assert resp.status_code == 200
assert resp.json() == {"reached_handler": True}


def test_redact_url_strips_access_token():
url = (
"https://graph.facebook.com/v24.0/me/adaccounts"
Expand Down
Loading