Skip to content

Commit 33ea007

Browse files
committed
Adds support for trusted-proxies property.
spring.cloud.gateway.trusted-proxies and spring.cloud.gateway.mvc.trusted-proxies
1 parent 738f2e8 commit 33ea007

File tree

25 files changed

+1193
-120
lines changed

25 files changed

+1193
-120
lines changed

docs/modules/ROOT/nav.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@
109109
*** xref:spring-cloud-gateway-server-mvc/filters/requestsize.adoc[]
110110
*** xref:spring-cloud-gateway-server-mvc/filters/setrequesthostheader.adoc[]
111111
*** xref:spring-cloud-gateway-server-mvc/filters/tokenrelay.adoc[]
112+
** xref:spring-cloud-gateway-server-mvc/httpheadersfilters.adoc[]
112113
** xref:spring-cloud-gateway-server-mvc/writing-custom-predicates-and-filters.adoc[]
113114
** xref:spring-cloud-gateway-server-mvc/working-with-servlets-and-filters.adoc[]
114115
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
[[httpheadersfilters]]
2+
= HttpHeadersFilters
3+
4+
HttpHeadersFilters are applied to the requests before sending them downstream, such as in the `NettyRoutingFilter`.
5+
6+
[[forwarded-headers-filter]]
7+
== Forwarded Headers Filter
8+
The `Forwarded` Headers Filter creates a `Forwarded` header to send to the downstream service. It adds the `Host` header, scheme and port of the current request to any existing `Forwarded` header. To activate this filter set the `spring.cloud.gateway.mvc.trusted-proxies` property to a Java Regular Expression. This regular expression defines the proxies that are trusted when they appear in the `Forwarded` header.
9+
10+
[[removehopbyhop-headers-filter]]
11+
== RemoveHopByHop Headers Filter
12+
The `RemoveHopByHop` Headers Filter removes headers from forwarded requests. The default list of headers that is removed comes from the https://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-14#section-7.1.3[IETF].
13+
14+
.The default removed headers are:
15+
* Connection
16+
* Keep-Alive
17+
* Proxy-Authenticate
18+
* Proxy-Authorization
19+
* TE
20+
* Trailer
21+
* Transfer-Encoding
22+
* Upgrade
23+
24+
//To change this, set the `spring.cloud.gateway.filter.remove-hop-by-hop.headers` property to the list of header names to remove.
25+
26+
[[xforwarded-headers-filter]]
27+
== XForwarded Headers Filter
28+
The `XForwarded` Headers Filter creates various `X-Forwarded-*` headers to send to the downstream service. It uses the `Host` header, scheme, port and path of the current request to create the various headers. To activate this filter set the `spring.cloud.gateway.mvc.trusted-proxies` property to a Java Regular Expression. This regular expression defines the proxies that are trusted when they appear in the `Forwarded` header.
29+
30+
Creating of individual headers can be controlled by the following boolean properties (defaults to true):
31+
32+
- `spring.cloud.gateway.x-forwarded.for-enabled`
33+
- `spring.cloud.gateway.x-forwarded.host-enabled`
34+
- `spring.cloud.gateway.x-forwarded.port-enabled`
35+
- `spring.cloud.gateway.x-forwarded.proto-enabled`
36+
- `spring.cloud.gateway.x-forwarded.prefix-enabled`
37+
38+
Appending multiple headers can be controlled by the following boolean properties (defaults to true):
39+
40+
- `spring.cloud.gateway.x-forwarded.for-append`
41+
- `spring.cloud.gateway.x-forwarded.host-append`
42+
- `spring.cloud.gateway.x-forwarded.port-append`
43+
- `spring.cloud.gateway.x-forwarded.proto-append`
44+
- `spring.cloud.gateway.x-forwarded.prefix-append`
45+

docs/modules/ROOT/pages/spring-cloud-gateway/httpheadersfilters.adoc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
[[forwarded-headers-filter]]
77
== Forwarded Headers Filter
8-
The `Forwarded` Headers Filter creates a `Forwarded` header to send to the downstream service. It adds the `Host` header, scheme and port of the current request to any existing `Forwarded` header.
8+
The `Forwarded` Headers Filter creates a `Forwarded` header to send to the downstream service. It adds the `Host` header, scheme and port of the current request to any existing `Forwarded` header. To activate this filter set the `spring.cloud.gateway.trusted-proxies` property to a Java Regular Expression. This regular expression defines the proxies that are trusted when they appear in the `Forwarded` header.
99

1010
[[removehopbyhop-headers-filter]]
1111
== RemoveHopByHop Headers Filter
@@ -25,7 +25,7 @@ To change this, set the `spring.cloud.gateway.filter.remove-hop-by-hop.headers`
2525

2626
[[xforwarded-headers-filter]]
2727
== XForwarded Headers Filter
28-
The `XForwarded` Headers Filter creates various `X-Forwarded-*` headers to send to the downstream service. It uses the `Host` header, scheme, port and path of the current request to create the various headers.
28+
The `XForwarded` Headers Filter creates various `X-Forwarded-*` headers to send to the downstream service. It uses the `Host` header, scheme, port and path of the current request to create the various headers. To activate this filter set the `spring.cloud.gateway.trusted-proxies` property to a Java Regular Expression. This regular expression defines the proxies that are trusted when they appear in the `Forwarded` header.
2929

3030
Creating of individual headers can be controlled by the following boolean properties (defaults to true):
3131

spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/GatewayServerMvcAutoConfiguration.java

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import org.springframework.cloud.gateway.server.mvc.filter.RemoveHopByHopResponseHeadersFilter;
4242
import org.springframework.cloud.gateway.server.mvc.filter.RemoveHttp2StatusResponseHeadersFilter;
4343
import org.springframework.cloud.gateway.server.mvc.filter.TransferEncodingNormalizationRequestHeadersFilter;
44+
import org.springframework.cloud.gateway.server.mvc.filter.TrustedProxies;
4445
import org.springframework.cloud.gateway.server.mvc.filter.WeightCalculatorFilter;
4546
import org.springframework.cloud.gateway.server.mvc.filter.XForwardedRequestHeadersFilter;
4647
import org.springframework.cloud.gateway.server.mvc.filter.XForwardedRequestHeadersFilterProperties;
@@ -50,6 +51,7 @@
5051
import org.springframework.cloud.gateway.server.mvc.predicate.PredicateDiscoverer;
5152
import org.springframework.context.ApplicationEventPublisher;
5253
import org.springframework.context.annotation.Bean;
54+
import org.springframework.context.annotation.Conditional;
5355
import org.springframework.context.annotation.Import;
5456
import org.springframework.core.env.Environment;
5557
import org.springframework.http.client.ClientHttpRequestFactory;
@@ -100,10 +102,9 @@ public FormFilter formFilter() {
100102

101103
@Bean
102104
@ConditionalOnMissingBean
103-
@ConditionalOnProperty(prefix = GatewayMvcProperties.PREFIX, name = "forwarded-request-headers-filter.enabled",
104-
matchIfMissing = true)
105-
public ForwardedRequestHeadersFilter forwardedRequestHeadersFilter() {
106-
return new ForwardedRequestHeadersFilter();
105+
@Conditional(TrustedProxies.ForwardedTrustedProxiesCondition.class)
106+
public ForwardedRequestHeadersFilter forwardedRequestHeadersFilter(GatewayMvcProperties properties) {
107+
return new ForwardedRequestHeadersFilter(properties.getTrustedProxies());
107108
}
108109

109110
@Bean
@@ -207,9 +208,10 @@ public WeightCalculatorFilter weightCalculatorFilter() {
207208
@ConditionalOnMissingBean
208209
@ConditionalOnProperty(prefix = XForwardedRequestHeadersFilterProperties.PREFIX, name = ".enabled",
209210
matchIfMissing = true)
210-
public XForwardedRequestHeadersFilter xForwardedRequestHeadersFilter(
211-
XForwardedRequestHeadersFilterProperties props) {
212-
return new XForwardedRequestHeadersFilter(props);
211+
@Conditional(TrustedProxies.XForwardedTrustedProxiesCondition.class)
212+
public XForwardedRequestHeadersFilter xForwardedRequestHeadersFilter(XForwardedRequestHeadersFilterProperties props,
213+
GatewayMvcProperties gatewayMvcProperties) {
214+
return new XForwardedRequestHeadersFilter(props, gatewayMvcProperties.getTrustedProxies());
213215
}
214216

215217
@Bean

spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/GatewayMvcProperties.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,12 @@ public class GatewayMvcProperties {
6565
*/
6666
private int streamingBufferSize = 16384;
6767

68+
/**
69+
* Regular expression defining proxies that are trusted when they appear in a
70+
* Forwarded of X-Forwarded header.
71+
*/
72+
private String trustedProxies;
73+
6874
public List<RouteProperties> getRoutes() {
6975
return routes;
7076
}
@@ -101,13 +107,22 @@ public void setStreamingBufferSize(int streamingBufferSize) {
101107
this.streamingBufferSize = streamingBufferSize;
102108
}
103109

110+
public String getTrustedProxies() {
111+
return trustedProxies;
112+
}
113+
114+
public void setTrustedProxies(String trustedProxies) {
115+
this.trustedProxies = trustedProxies;
116+
}
117+
104118
@Override
105119
public String toString() {
106120
return new ToStringCreator(this).append("httpClient", httpClient)
107121
.append("routes", routes)
108122
.append("routesMap", routesMap)
109123
.append("streamingMediaTypes", streamingMediaTypes)
110124
.append("streamingBufferSize", streamingBufferSize)
125+
.append("trustedProxies", trustedProxies)
111126
.toString();
112127
}
113128

spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/ForwardedRequestHeadersFilter.java

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,12 @@
2424
import java.util.List;
2525
import java.util.Map;
2626

27+
import org.apache.commons.logging.Log;
28+
import org.apache.commons.logging.LogFactory;
29+
30+
import org.springframework.cloud.gateway.server.mvc.config.GatewayMvcProperties;
2731
import org.springframework.core.Ordered;
32+
import org.springframework.core.log.LogMessage;
2833
import org.springframework.http.HttpHeaders;
2934
import org.springframework.util.CollectionUtils;
3035
import org.springframework.util.LinkedCaseInsensitiveMap;
@@ -34,20 +39,38 @@
3439

3540
public class ForwardedRequestHeadersFilter implements HttpHeadersFilter.RequestHttpHeadersFilter, Ordered {
3641

42+
private static final Log log = LogFactory.getLog(ForwardedRequestHeadersFilter.class);
43+
3744
/**
3845
* Forwarded header.
3946
*/
4047
public static final String FORWARDED_HEADER = "Forwarded";
4148

49+
private final TrustedProxies trustedProxies;
50+
51+
@Deprecated
52+
public ForwardedRequestHeadersFilter() {
53+
trustedProxies = s -> true;
54+
log.warn(GatewayMvcProperties.PREFIX
55+
+ ".trusted-proxies is not set. Using deprecated Constructor. Untrusted hosts might be added to Forwarded header.");
56+
}
57+
58+
public ForwardedRequestHeadersFilter(String trustedProxiesRegex) {
59+
trustedProxies = TrustedProxies.from(trustedProxiesRegex);
60+
}
61+
4262
/* for testing */
4363
static List<Forwarded> parse(List<String> values) {
4464
ArrayList<Forwarded> forwardeds = new ArrayList<>();
4565
if (CollectionUtils.isEmpty(values)) {
4666
return forwardeds;
4767
}
4868
for (String value : values) {
49-
Forwarded forwarded = parse(value);
50-
forwardeds.add(forwarded);
69+
String[] forwardedValues = StringUtils.tokenizeToStringArray(value, ",");
70+
for (String forwardedValue : forwardedValues) {
71+
Forwarded forwarded = parse(forwardedValue);
72+
forwardeds.add(forwarded);
73+
}
5174
}
5275
return forwardeds;
5376
}
@@ -89,6 +112,13 @@ public int getOrder() {
89112

90113
@Override
91114
public HttpHeaders apply(HttpHeaders input, ServerRequest request) {
115+
if (request.servletRequest().getRemoteAddr() != null
116+
&& !trustedProxies.isTrusted(request.servletRequest().getRemoteAddr())) {
117+
log.trace(LogMessage.format("Remote address not trusted. pattern %s remote address %s", trustedProxies,
118+
request.servletRequest().getRemoteHost()));
119+
return input;
120+
}
121+
92122
HttpHeaders original = input;
93123
HttpHeaders updated = new HttpHeaders();
94124

@@ -102,7 +132,10 @@ public HttpHeaders apply(HttpHeaders input, ServerRequest request) {
102132
List<Forwarded> forwardeds = parse(original.get(FORWARDED_HEADER));
103133

104134
for (Forwarded f : forwardeds) {
105-
updated.add(FORWARDED_HEADER, f.toHeaderValue());
135+
// only add if "for" value matches trustedProxies
136+
if (trustedProxies.isTrusted(f.get("for"))) {
137+
updated.add(FORWARDED_HEADER, f.toHeaderValue());
138+
}
106139
}
107140

108141
// TODO: add new forwarded
@@ -124,6 +157,10 @@ public HttpHeaders apply(HttpHeaders input, ServerRequest request) {
124157
forValue = "[" + forValue + "]";
125158
}
126159
}
160+
if (!trustedProxies.isTrusted(forValue)) {
161+
// don't add for value
162+
return;
163+
}
127164
int port = remoteAddress.getPort();
128165
if (port >= 0) {
129166
forValue = forValue + ":" + port;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/*
2+
* Copyright 2013-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cloud.gateway.server.mvc.filter;
18+
19+
import java.lang.annotation.Documented;
20+
import java.lang.annotation.ElementType;
21+
import java.lang.annotation.Retention;
22+
import java.lang.annotation.RetentionPolicy;
23+
import java.lang.annotation.Target;
24+
import java.util.NoSuchElementException;
25+
import java.util.regex.Pattern;
26+
27+
import org.springframework.boot.autoconfigure.condition.AllNestedConditions;
28+
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
29+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
30+
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
31+
import org.springframework.cloud.gateway.server.mvc.config.GatewayMvcProperties;
32+
import org.springframework.context.annotation.ConditionContext;
33+
import org.springframework.context.annotation.Conditional;
34+
import org.springframework.core.type.AnnotatedTypeMetadata;
35+
import org.springframework.lang.NonNull;
36+
import org.springframework.util.Assert;
37+
import org.springframework.util.StringUtils;
38+
39+
@FunctionalInterface
40+
public interface TrustedProxies {
41+
42+
boolean isTrusted(String host);
43+
44+
static TrustedProxies from(@NonNull String trustedProxies) {
45+
Assert.hasText(trustedProxies, "trustedProxies must not be empty");
46+
Pattern pattern = Pattern.compile(trustedProxies);
47+
return value -> pattern.matcher(value).matches();
48+
}
49+
50+
class ForwardedTrustedProxiesCondition extends AllNestedConditions {
51+
52+
public ForwardedTrustedProxiesCondition() {
53+
super(ConfigurationPhase.REGISTER_BEAN);
54+
}
55+
56+
@ConditionalOnProperty(name = GatewayMvcProperties.PREFIX + ".forwarded-request-headers-filter.enabled",
57+
matchIfMissing = true)
58+
static class OnPropertyEnabled {
59+
60+
}
61+
62+
@ConditionalOnPropertyExists(GatewayMvcProperties.PREFIX + ".trusted-proxies")
63+
static class OnTrustedProxiesNotEmpty {
64+
65+
}
66+
67+
}
68+
69+
class XForwardedTrustedProxiesCondition extends AllNestedConditions {
70+
71+
public XForwardedTrustedProxiesCondition() {
72+
super(ConfigurationPhase.REGISTER_BEAN);
73+
}
74+
75+
@ConditionalOnProperty(name = XForwardedRequestHeadersFilterProperties.PREFIX + ".enabled",
76+
matchIfMissing = true)
77+
static class OnPropertyEnabled {
78+
79+
}
80+
81+
@ConditionalOnPropertyExists(GatewayMvcProperties.PREFIX + ".trusted-proxies")
82+
static class OnTrustedProxiesNotEmpty {
83+
84+
}
85+
86+
}
87+
88+
class OnPropertyExistsCondition extends SpringBootCondition {
89+
90+
@Override
91+
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
92+
try {
93+
String value = metadata.getAnnotations().get(ConditionalOnPropertyExists.class).getString("value");
94+
String property = context.getEnvironment().getProperty(value);
95+
if (!StringUtils.hasText(property)) {
96+
return ConditionOutcome.noMatch(value + " property is not set or is empty.");
97+
}
98+
return ConditionOutcome.match(value + " property is not empty.");
99+
}
100+
catch (NoSuchElementException e) {
101+
return ConditionOutcome.noMatch("Missing required property 'value' of @ConditionalOnPropertyExists");
102+
}
103+
}
104+
105+
}
106+
107+
@Retention(RetentionPolicy.RUNTIME)
108+
@Target({ ElementType.TYPE, ElementType.METHOD })
109+
@Documented
110+
@Conditional(OnPropertyExistsCondition.class)
111+
@interface ConditionalOnPropertyExists {
112+
113+
/**
114+
* @return the property
115+
*/
116+
String value();
117+
118+
}
119+
120+
}

0 commit comments

Comments
 (0)