4 min readSSRF · API-SECURITY · WEB

Secondary-context attacks: when a public API is a proxy in disguise

A target-free walkthrough of BFF and gateway abuse: a public endpoint silently forwards to an internal service, and path traversal in a URL segment reaches endpoints that were never meant to be public.

client public API BFF / REVERSE PROXY PROXIES TO internal service /v1/:service/ ..%2f../ ESCAPES ROUTE internal endpoints NEVER EXPOSED
A public API that proxies to an internal service it never meant to expose

Half the value in finding this bug is realising the API you’re hitting isn’t the real server. It’s a forwarder. You send a request to a public host, and behind the scenes some gateway takes a piece of your URL and stitches it into a request to an internal service the public internet was never meant to reach. The “primary context” is the public API’s routing; the “secondary context” is the internal request it builds from your input. The bug lives in the gap where the second context interprets bytes the first one passed through unexamined.

Spotting the proxy

You can’t attack a forwarder you haven’t noticed. The tells are in the URL shape and in the response behaviour. Path patterns that should make you stop:

plaintext
/bff/account/profile
/proxy/orders/12345
/api/v1/payments/charges
/gateway/inventory/sku/abc
/internal/users/me

Anything with bff, proxy, gateway, internal, or a segment that looks like a service name (payments, inventory, orders) sitting in the middle of the path is a candidate. A BFF (backend for frontend) is the textbook case: the frontend talks to one friendly API that fans out to many internal services, and the service name is right there in the route.

The behavioural tells confirm it. Response headers that leak a different upstream (Server, Via, an internal X- header), error pages that don’t match the public app’s styling, or latency that jumps when one route fans out further than another. Those say “something downstream is answering, not this host.”

Testing the seam

Once you believe a segment is forwarded, the test is whether you can break out of the intended path. The gateway builds something like http://orders.internal/v1/<your-segment>, so the question is whether your segment can climb back up and across.

Start with traversal sequences in the forwarded segment, encoded so the public router passes them through and the upstream resolves them:

http
GET /api/v1/orders/..%2f..%2finventory%2fadmin%2fflush HTTP/1.1
Host: public.example

Layered encoding matters because the two contexts decode a different number of times. Try, in order: a literal ../, single-encoded %2e%2e%2f, double-encoded %252e%252e%252f, and the mixed ..%2f. Each one targets a different decode depth in the chain. When one of them suddenly returns a response that doesn’t belong to the route you asked for (a different service’s 404, an unexpected JSON shape, an auth error from a system you never named), you’ve crossed the boundary.

The high-value pivots once you’re across:

  • Reach a sibling service’s internal API directly, skipping the public gateway’s auth layer.
  • Hit admin or debug routes (/admin, /actuator, /_internal, /debug) that trust their network position and never check a token.
  • Reach the service’s own metadata or health endpoints, which often spill config, versions, and sometimes credentials.

Why this isn’t quite SSRF

It reads like SSRF because you end up talking to an internal host, but you don’t control the destination host; you control a path inside a destination the gateway already chose. That distinction matters for both testing and reporting. You’re not smuggling a 169.254.169.254 into a fetch parameter. You’re abusing the trust an internal service places in its gateway, the assumption that “anything reaching me has already been authorised, because only the gateway can reach me.” That assumption is the actual vulnerability; the traversal is just how you violate it.

When I report one of these, the load-bearing evidence is a single request whose response unmistakably comes from a different service than the route advertised. A triager who sees a payments-service error returned from an orders route, with a traversal sequence in the path, doesn’t need the architecture diagram. The crossed boundary is right there in the diff.

The lesson that outlives any one stack: the moment you see a service name in a URL path on a public API, assume you’re talking to a proxy, and test whether you can steer where it forwards. Gateways were built to simplify routing, not to be a security boundary, and most internal services are still trusting them to be one.