Skip to content

Add support for X-Forwarded-Prefix #14111

Description

@mkurz

Note

This issue is just to gather feedback if people really need that.

Play should support applications published below a request-specific public path prefix that a trusted reverse proxy removes before forwarding the request to Play.

The eventual X-Forwarded-Prefix support should be built on a general public request pathBase model. It should not rewrite the backend request target or reuse play.http.context as a substitute for dynamic request metadata.

Motivation

Consider a public request to:

https://example.com/store/products/42

The reverse proxy mounts the Play application below /store, strips that prefix, and forwards:

GET /products/42 HTTP/1.1
Host: play.internal
X-Forwarded-Prefix: /store

Play must continue routing /products/42, while request-aware links and redirects intended for the client need to use /store/products/42.

play.http.context does not model this correctly. It is a static application and routing context that is expected to be present in the path received by Play. A forwarded prefix is request-specific public metadata for a path component that has already been removed upstream.

Proposed Request Model

Introduce a general path-base property before interpreting the proxy header:

request.pathBase // "/store"
request.path     // "/products/42"

with a Java equivalent:

request.pathBase();

A derived public path may also be useful:

request.publicPath // "/store/products/42"

The name and necessity of publicPath remain an API design decision; pathBase is the essential model.

The following values must retain backend semantics:

  • request.path
  • request.uri
  • request.target
  • router matching

pathBase must be carried separately and preserved through Scala and Java request implementations, wrappers, builders, fake requests, copy methods, server conversions, and error requests.

It should not be stored by mutating RequestTarget, because that represents the target received by Play after proxy processing.

Suggested Delivery

This should preferably be delivered as two reviewable pull requests.

1. Introduce RequestHeader.pathBase

The first PR should add the general request metadata independently of any specific proxy header.

It should:

  • Default pathBase to the empty string.
  • Define and enforce normalization and validation rules.
  • Preserve backend path, URI, target, and routing behavior.
  • Add Scala and Java APIs and builders.
  • Define safe composition of pathBase with a backend path.
  • Integrate request-aware URL generation and redirects.
  • Document the distinction from play.http.context.

Request-aware APIs that should be audited include:

  • Scala Call.absoluteURL and Call.webSocketURL.
  • Java Call.absoluteURL(request) and Call.webSocketURL(request).
  • Call.relative and Java relativeTo.
  • HTTPS redirects and helpers that reconstruct the current public URL.
  • Equivalent Scala and Java APIs.
  • Cookie-path behavior and any cache keys derived from request paths.

Plain generated Call.url has no RequestHeader, so a dynamic path base cannot be applied implicitly. It should remain compatible unless a clear request-aware API is introduced. The PR must document this boundary rather than making only some route URLs change invisibly.

2. Map trusted X-Forwarded-Prefix to pathBase

The second PR should add an explicit opt-in, for example:

play.http.forwarded.trustXForwardedPrefix = false

The default must remain false.

Suggested behavior:

  • Apply only to Play's x-forwarded forwarding mode. RFC 7239 has no prefix parameter.
  • Require the directly connected peer to be a configured trusted proxy.
  • Accept exactly one authoritative, non-empty, valid value.
  • Work without X-Forwarded-For; the direct trusted peer is authoritative for this independent metadata.
  • Reject or ignore repeated fields, comma-separated lists, malformed values, and ambiguous input according to a documented fail-closed policy.
  • Populate only request.pathBase.
  • Never modify request.path, request.uri, request.target, or routing.
  • Keep the opt-in independent from forwarded proto, host, and port options.
  • Leave the raw header available through request.headers, consistently with Play's other forwarded metadata.

The implementation must reuse Play's direct-peer trust boundary and strict single-value handling patterns rather than selecting an arbitrary first value from an appended list.

Validation And Normalization

The path-base PR should settle these cases before the header parser is added:

  • Empty path base.
  • / and whether it canonicalizes to the empty base.
  • Required leading slash.
  • Trailing slash normalization.
  • Repeated slashes and dot segments.
  • Percent-encoded bytes, including encoded separators.
  • Query, fragment, backslash, control-character, and invalid UTF-8 input.
  • Composition with a backend root path.
  • Composition with a non-root play.http.context.
  • Non-path request targets such as * and authority-form CONNECT.

Avoid decoding and re-encoding a valid prefix in a way that changes its public URL meaning. Use structured URI/path parsing where practical rather than ad hoc string replacement.

Security

Trusting X-Forwarded-Prefix is security-sensitive. Documentation must state
that:

  • Untrusted clients must not be able to connect directly to the Play server.
  • The trusted edge proxy must remove or overwrite any client-supplied X-Forwarded-Prefix value.
  • A Play trust option cannot repair a deployment where the proxy appends to an attacker-controlled value.
  • The value can affect generated links, redirects, cookie scope, cache keys, and externally visible URLs.

Symfony's CVE-2021-41267 demonstrates the cache-poisoning risk of trusting unsanitized X-Forwarded-Prefix. The initial implementation should therefore prefer one sanitized value from the direct trusted proxy over multi-hop or first-value heuristics.

Compatibility

  • Existing applications must keep the empty path base by default.
  • X-Forwarded-Prefix must have no effect until explicitly enabled.
  • Existing backend routing and play.http.context behavior must remain unchanged.
  • Existing Call.url output should not change merely because a request has a dynamic path base unless a new request-aware API explicitly requests that behavior.

Test Coverage

The path-base API should cover:

  • Empty and non-empty path bases in Scala and Java.
  • Builders, fake requests, wrappers, conversions, copies, and error requests.
  • Backend path, URI, target, and routing remaining unchanged.
  • Public-path composition.
  • Absolute HTTP and WebSocket URLs.
  • Relative URLs and redirects.
  • Interaction with play.http.context.
  • The chosen normalization and invalid-value behavior.

The header integration should additionally cover:

  • Default-disabled behavior.
  • One valid value from a trusted direct proxy.
  • Operation without X-Forwarded-For.
  • An untrusted direct peer.
  • Repeated physical fields and comma-separated values.
  • Empty, malformed, and ambiguous values.
  • Independent composition with trusted forwarded scheme and authority.
  • Netty and Pekko HTTP integration.
  • Explicit regression coverage for header injection and public URL/cache
    poisoning scenarios.

Non-Goals For The Initial Implementation

  • Do not resurrect the old design that requires the value to equal play.http.context.
  • Do not prepend the prefix to the backend request target or router input.
  • Do not implement multi-hop or appended-prefix selection without a separate trusted-hop design.
  • Do not add an RFC 7239 extension parameter and call it standardized prefix support.
  • Do not automatically rewrite application cookie paths without an explicit, documented API decision.

References

Acceptance Criteria

  • Play has a documented request-level path-base model independent of routing.
  • Trusted X-Forwarded-Prefix can populate it without X-Forwarded-For.
  • The feature is disabled and behavior-compatible by default.
  • Only one sanitized value from a trusted direct peer can affect public path metadata.
  • Backend paths and routing remain unchanged.
  • Request-aware public URLs and redirects include the path base consistently.
  • Scala, Java, Netty, Pekko HTTP, security, migration, and production documentation are covered.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions