skip to content

When generating absolute URLs behind a reverse proxy, how do forwarded headers affect ServletUriComponentsBuilder, and what are the security implications?

level: principalimportance: should knowfreq 16%

answer

  1. proxy -> app sees internal host -> wrong absolute URLs
  2. ForwardedHeaderFilter applies Forwarded / X-Forwarded-*
  3. server.forward-headers-strategy = framework|native|none
  4. headers are client-settable -> host-header injection
  5. trust only behind trusted proxy; config base URL for reset/OAuth

basics

~20 s

Behind a proxy the app sees the internal host, so generated URLs are wrong unless it reads Forwarded / X-Forwarded-* headers. Registering Spring's ForwardedHeaderFilter makes ServletUriComponentsBuilder use them. But those headers are client-controllable, so only trust them behind a trusted proxy to avoid host-header/link poisoning.

solid answer

~50 s

A reverse proxy terminates TLS and forwards to the app over http://internal:8080, so the HttpServletRequest reports the internal scheme/host/port. ServletUriComponentsBuilder.fromCurrentRequest() would then emit internal URLs in Location headers and links. The fix is to honor the RFC 7239 Forwarded header (and de-facto X-Forwarded-Host/Proto/Port/Prefix) by registering Spring's ForwardedHeaderFilter as a Filter bean; it wraps the request so scheme/host/port/contextPath reflect the external address, and every UriComponentsBuilder built from the request inherits that. The security catch: these headers are attacker-settable. If your app is directly reachable, or the proxy doesn't strip/overwrite inbound forwarded headers, a client can forge Host to poison password-reset links, cache keys, or redirects (host-header injection). Mitigate by terminating forwarded-header trust at a trusted proxy that overwrites them, or use ForwardedHeaderFilter with REMOVE_ONLY mode where you don't want to trust them, and validate/whitelist the host.

code

java · 23 lines
java
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.*;
import org.springframework.core.Ordered;
import org.springframework.web.filter.ForwardedHeaderFilter;

@Configuration
class WebConfig {

    // Trust proxy-supplied Forwarded / X-Forwarded-* so absolute URLs match the
    // external host. Only safe when a TRUSTED proxy overwrites these headers and
    // the app is not directly reachable.
    @Bean
    FilterRegistrationBean<ForwardedHeaderFilter> forwardedHeaderFilter() {
        var filter = new ForwardedHeaderFilter();
        // If you must NOT trust them, strip instead of honoring:
        // filter.setRemoveOnly(true);
        var reg = new FilterRegistrationBean<>(filter);
        reg.setOrder(Ordered.HIGHEST_PRECEDENCE);
        return reg;
    }
}
// Spring Boot alternative: application.yml
//   server.forward-headers-strategy: framework   # uses ForwardedHeaderFilter

go deeper

for a junior

Aware that behind a proxy the generated host can be wrong and Spring has a filter to fix it.

for a middle

Can register ForwardedHeaderFilter or set server.forward-headers-strategy so absolute URLs reflect the external host.

for a senior

Understands the header set (Forwarded/X-Forwarded-*), how it flows into ServletUriComponentsBuilder, and the host-header spoofing risk.

for a principal

Owns the trust boundary: overwrite-at-trusted-proxy, REMOVE_ONLY/native RemoteIp trusted-proxy config, and configured external base URLs for high-value links; weighs relative URLs to avoid host reconstruction entirely.

## The scenario A typical deployment: client -> `https://public.example.com` (TLS) -> reverse proxy / load balancer -> `http://10.0.0.5:8080` app. From the app's `HttpServletRequest`, `getScheme()` is `http`, `getServerName()` is the internal host, `getServerPort()` is 8080. So `ServletUriComponentsBuilder.fromCurrentRequest()` builds `http://10.0.0.5:8080/...` — wrong for anything the client will use (Location headers, absolute links, password-reset emails, OAuth redirect URIs). ## Forwarded headers Proxies advertise the original request via headers: - **`Forwarded`** (RFC 7239): `Forwarded: for=...; host=public.example.com; proto=https`. - **De-facto `X-Forwarded-*`**: `X-Forwarded-Host`, `X-Forwarded-Proto`, `X-Forwarded-Port`, `X-Forwarded-For`, and `X-Forwarded-Prefix` (path prefix stripped by the proxy). ## ForwardedHeaderFilter Spring's `org.springframework.web.filter.ForwardedHeaderFilter` reads these headers and returns a **wrapped request** whose `getScheme()`/`getServerName()`/`getServerPort()`/`getContextPath()` reflect the external values. Because `ServletUriComponentsBuilder` reads the (now-wrapped) request, all generated URIs become externally correct automatically — no code change in controllers. Register it as a filter bean: ```java @Bean FilterRegistrationBean<ForwardedHeaderFilter> forwardedHeaderFilter() { var reg = new FilterRegistrationBean<>(new ForwardedHeaderFilter()); reg.setOrder(Ordered.HIGHEST_PRECEDENCE); // run before others return reg; } ``` Spring Boot can also enable it via `server.forward-headers-strategy=framework` (uses ForwardedHeaderFilter) or `=native` (delegates to the servlet container, e.g. Tomcat's RemoteIpValve). `=none` disables it. ## The security problem — host-header / forwarded-header injection These headers are just request headers; **any client can set them**. If your app trusts them but is reachable without passing through the trusted proxy (or the proxy forwards client-supplied values instead of overwriting), an attacker sends `X-Forwarded-Host: evil.com`. Now: - A password-reset email built with `fromCurrentRequest()` links to `https://evil.com/reset?token=...` -> token theft. - Absolute redirects / OAuth redirect URIs point at the attacker. - Web-cache poisoning if the generated URL is cached. This is classic **host-header injection**. ## Mitigations (principal-level judgment) 1. **Terminate trust at a trusted proxy** that *overwrites* (not appends) `X-Forwarded-*`/`Forwarded` with authoritative values and blocks direct access to the app. 2. **ForwardedHeaderFilter modes**: it can be configured to `setRemoveOnly(true)` — strip forwarded headers and ignore them — when the app should not trust them at all. Also `setRelativeRedirects(true)` to emit relative redirects and sidestep host reconstruction for redirects. 3. **Native strategy with RemoteIpValve/RemoteIpFilter** lets you configure `trustedProxies`/`internalProxies` regex so only known proxy IPs are honored. 4. **Validate/whitelist** the resulting host for security-sensitive URLs (reset links, redirects) instead of blindly trusting the reconstructed value. 5. Prefer **relative URLs** where you can (same-origin links don't need the host at all). ## Interaction with the thread-local `fromCurrentRequest()` still relies on `RequestContextHolder`'s thread-local, so this all applies on the request thread. Off-thread (async email sending), capture the reconstructed base URL on the request thread or configure an explicit external base URL in config rather than deriving it from a request — often the safest choice for emails/OAuth is a **statically configured external base URL** so you don't depend on request headers at all. ## EncodingMode note Orthogonal but related: `ServletUriComponentsBuilder` is still a `UriComponentsBuilder`, so the encoding rules (TEMPLATE_AND_VALUES etc.) apply to whatever path/query you append. ## Summary judgment Use `ForwardedHeaderFilter` (or `server.forward-headers-strategy`) so generated URLs match the external address, but treat forwarded headers as **untrusted input** unless a trusted proxy authoritatively sets them. For high-value links (password reset, OAuth), prefer a configured external base URL over request-derived hosts.

  • For password-reset emails, why might deriving the base URL from the request be a bad idea even with ForwardedHeaderFilter?
    The host still comes from client-influenceable headers; a spoofed X-Forwarded-Host produces a reset link to an attacker domain and leaks the token. Emails are also sent off the request thread. Prefer a statically configured external base URL.
  • What does server.forward-headers-strategy control in Spring Boot?
    How forwarded headers are handled: 'framework' registers ForwardedHeaderFilter (Spring reconstructs scheme/host/port/prefix), 'native' delegates to the servlet container (e.g. Tomcat RemoteIpValve with trusted-proxy config), and 'none' ignores them.

saying these in an interview costs you the question

  • Trusting X-Forwarded-Host / Forwarded headers when the app is directly reachable or the proxy doesn't overwrite them.
  • Assuming Spring honors forwarded headers by default without any configuration.
  • Building password-reset / OAuth redirect URLs from fromCurrentRequest() and treating the host as trusted.
  • Confusing ForwardedHeaderFilter (reconstructs URL) with something that authenticates the proxy — it does not verify who set the headers.

context