Skip to content

EPP does not strip client-supplied x-prefiller-host-port on decode-only requests (SSRF from the decode sidecar) #3087

Description

@satyamg1620

What happened:

When the EPP routes a request decode-only, a client-supplied x-prefiller-host-port header reaches the P/D sidecar unchanged. The sidecar sends the request body to that host and port and returns that host's response body to the client. That lets a client send requests to any address reachable from the decode pod's network, such as other pods, cluster services, or node-local endpoints.

The EPP routes decode-only for short prompts, cached prompts, and any request the PD decider declines. The client can trigger this path by sending a short prompt. When the EPP selects a prefill endpoint, its value overwrites the client's, so prefill-decode requests are not affected.

The sidecar allowlist (--enable-ssrf-protection) blocks this, but it defaults to false, and no config under config/ or deploy/ enables it.

What you expected to happen:

The EPP owns the routing headers. A client-supplied value is removed from the request before Envoy forwards it, whether or not the scheduler sets the header.

How to reproduce it (as minimally and precisely as possible):

Setup: a P/D deployment with the disagg-profile-handler and prefix-based-pd-decider (nonCachedTokens: 16), and a decode pod running the sidecar with --kv-connector=nixlv2 and the default --enable-ssrf-protection=false. The target below is the EPP pod's metrics port, standing in for an internal service.

curl -s localhost:18081/v1/completions \
  -H 'content-type: application/json' \
  -H 'x-prefiller-host-port: 10.244.0.113:9090' \
  -d '{"model":"Qwen/Qwen3-8B","prompt":"Hi there","max_tokens":4}'
# HTTP 404, body "404 page not found", returned by the EPP metrics server

Decode sidecar log for that request:

"SSRF protection: prefill target allowed" target=10.244.0.113:9090
"running NIXL protocol V2" url=10.244.0.113:9090
"sending prefill request" to=10.244.0.113:9090
"prefill request failed"

Other routing headers the sidecar reads, from the same test:

Header Reaches sidecar Effect in this setup
x-prefiller-host-port Yes Request sent to the client-chosen host; response returned to the client
x-encoder-hosts-ports Yes Accepted as encoder targets; not used only because the request had no multimodal content
x-data-parallel-host-port Yes Request fails with 400
x-kv-cache-source-host-port Yes Ignored by the nixlv2 connector

Anything else we need to know?:

Root cause: disagg.Handler.PreRequest removes the header from the EPP's copy of the request:

// pkg/epp/framework/plugins/scheduling/profilehandler/disagg/disagg_profile_handler.go:520
delete(request.Headers, routing.PrefillEndpointHeader)

The ext_proc request-headers response sets headers only:

// pkg/epp/handlers/request.go:98-100
HeaderMutation: &extProcPb.HeaderMutation{
    SetHeaders: s.generateHeaders(ctx, reqCtx),
},

A deleted key is never set again, and nothing tells Envoy to remove it, so Envoy forwards the client's value. x-encoder-hosts-ports, deleted at line 545, has the same problem. Nothing in pkg/epp sets HeaderMutation.RemoveHeaders.

Proposed fix: in the request-headers response, add the EPP-owned routing headers (x-prefiller-host-port, x-encoder-hosts-ports, x-data-parallel-host-port, x-kv-cache-source-host-port) to HeaderMutation.RemoveHeaders whenever the scheduler did not set them. A broader option is to remove every incoming header key that PreRequest deleted from reqCtx.Request.Headers. The first test would be a handler test that sends the header, runs a decode-only schedule, and asserts the key appears in RemoveHeaders.

Mitigation until fixed: run the sidecar with --enable-ssrf-protection=true and --inference-pool set. #979 notes that the allowlist matches pod IP only, not host:port.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs-triageIndicates an issue or PR lacks a triage label and requires one.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions