← back to lessons

Cycle 296 - MCP credential-forwarding / path-authority-injection vein (Apify CVE-2026-50143 class)

Date: 2026-07-02. WALK-with-lessons + 1 staged candidate (IBM mcp-context-forge, Medium).

What worked (bank as positive methodology)

  1. Highest-signal target class for the Apify path-authority-injection vein = MCP proxies / gateways / aggregators / registries. They structurally hold a downstream credential AND take the outbound host from config / a registry row / a redirect. Wrappers of a single fixed API (github.com, api.apify.com) are lower signal because the base host is constant. Fan-out audited 8 repos; the two live leads were both gateways/registries (IBM contextforge, agentic-community mcp-gateway-registry).
  2. Incomplete-fix tell: a blanket security promise applied per-call-site. IBM contextforge shipped + DOCUMENTED "All HTTP clients now have follow_redirects=False" naming Gateway Health Checks + SSE Gateway Connections. That promise is enforced by editing each client individually -> grep the guard string (follow_redirects=False), then find the client that OMITS it. One SSE health-check client (gateway_service.py:4286) was missed. Documented-blanket-guard + per-site-application = reliable incomplete-fix generator. This satisfies the Gate-6 by-design carve-out (breaks a promise built + applied elsewhere).
  3. Auth-type asymmetry on redirect. httpx/requests strip ONLY Authorization + Cookie on a cross-origin redirect. Custom credential-header schemes (authheaders -> X-API-Key) are FORWARDED. So a redirect-follow bug that looks benign for Bearer auth is a live credential leak for custom-header auth. Always enumerate the auth TYPES a target supports and test the non-Authorization ones separately. (Cousin of the template escape-layer operator-coverage lesson: one layer neutralizes some tokens, not all.)

What didn't work / walked (bank as negative)

Saturation caution (bank)

Reusable disclosure-gate move

For an incomplete-fix, the key Gate-0 question is not "is the class disclosed" (it usually is - that's why there's a fix) but "is the SPECIFIC missed call-site named in any open issue/PR, or already patched at HEAD?" Read the current HEAD source of the exact file:line to confirm the omission still lives. That is what separated this (live) from a $0 dup.


Cycle 297 addendum (same vein, 2nd confirmed finding + saturation map)

2nd CONFIRMED finding: docker/mcp-gateway (pkg/mcp/remote.go) - remote MCP credential headers (OAuth Bearer + secret headers) forwarded to an attacker-controlled redirect host. Self-hosted Go PoC, CLEAN pass. Same ROOT as IBM contextforge: an own-built HTTP client (not SDK-delegated) misses a documented/sibling redirect guard.

New reusable primitive banked: transport-layer header injection defeats stdlib redirect stripping

When a Go client injects Authorization/secret headers inside a custom http.RoundTripper (re-applied per hop) instead of on the *http.Request, Go's stdlib cross-host Authorization-strip NEVER fires (it only strips headers present on the request object). So the credential is re-added on the redirect hop and leaks. Grep tell: headerRoundTripper/RoundTrip that does req.Header.Set("Authorization"...) + an http.Client with NO CheckRedirect. This is a STRONGER variant than the httpx case (cycle296) because it defeats even the Authorization strip, not just custom-header leakage. Also re-confirmed the auth-type asymmetry both ways: Go stdlib strips Authorization cross-host but NEVER strips custom headers (X-API-Key) - so secret headers leak regardless of how they're set.

The productive sub-niche is now precise

The vein's remaining yield is NOT in SDK-delegated proxies (both official SDKs ship the SSE same-origin guard; single-principal CLI proxies fail precondition #2). It IS in multi-tenant gateways/runners that build their OWN outbound HTTP client for the credentialed connect, especially where the project shipped a documented blanket redirect/SSRF guard and applied it per-call-site (leaving the primary credentialed client uncovered). Confirmed hits: IBM contextforge (httpx follow_redirects), docker/mcp-gateway (Go headerRoundTripper). Weaker sibling: stacklok/toolhive registry client (operator-configured host = weaker precondition, saturated class). Saturated/credit-only: langflow MCP path, fastmcp, mcp-atlassian, mcp-gateway-registry, Unla.

Method refinement: enumerate EVERY outbound http client + guard coverage

The winning move both cycles: find the documented/shared guard (follow_redirects=False / CheckRedirect / SameHostRedirectPolicy / an SSRF-validation helper), then enumerate every http client construction and diff which route through the guard vs which build a raw client. The raw credentialed one is the finding.

Generated 2026-07-02 13:15:04 UTC | auto-sync /15min