Skip to content

Commit b435ce3

Browse files
authored
Prevent Content-Length and Host headers from being copied by default (#3313)
Also remove the bogus spring.cloud.gateway.proxy.auto-forward setting from the tests. Fixes gh-3154
1 parent 17c1b5b commit b435ce3

File tree

12 files changed

+158
-64
lines changed

12 files changed

+158
-64
lines changed

docs/modules/ROOT/pages/spring-cloud-gateway-proxy-exchange.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,5 +76,5 @@ You can add headers to the downstream response by using the `header()` methods o
7676
You can also manipulate response headers (and anything else you like in the response) by adding a mapper to the `get()` method (and other methods).
7777
The mapper is a `Function` that takes the incoming `ResponseEntity` and converts it to an outgoing one.
7878

79-
First-class support is provided for "`sensitive`" headers (by default, `cookie` and `authorization`), which are not passed downstream, and for "`proxy`" (`x-forwarded-*`) headers.
79+
First-class support is provided for "`sensitive`" headers (by default, `cookie` and `authorization`) and "`skipped`" headers (by default, `content-length` and `host`), which are not passed downstream, and for "`proxy`" (`x-forwarded-*`) headers. The idea behind "`skipped`" headers is that they may result in problems when copied over to the downstream request. For example: because of the way that the `ProxyExchange` calls the downstream endpoint the content's length might have changed or even use a `Transfer-Encoding: chunked` instead of a `Content-Length` header.
8080

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

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,11 @@
8585
*
8686
* <p>
8787
* By default the incoming request body and headers are sent intact to the downstream
88-
* service (with the exception of "sensitive" headers). To manipulate the downstream
88+
* service (with the exception of "excluded" headers). To manipulate the downstream
8989
* request there are "builder" style methods in {@link ProxyExchange}, but only the
90-
* {@link #uri(String)} is mandatory. You can change the sensitive headers by calling the
91-
* {@link #sensitive(String...)} method (Authorization and Cookie are sensitive by
92-
* default).
90+
* {@link #uri(String)} is mandatory. You can change the excluded headers by calling the
91+
* {@link #excluded(String...)} method (the argument resolver will populate these with
92+
* some sensible defaults).
9393
* </p>
9494
* <p>
9595
* The type parameter <code>T</code> in <code>ProxyExchange&lt;T&gt;</code> is the type of
@@ -137,12 +137,6 @@
137137
*/
138138
public class ProxyExchange<T> {
139139

140-
/**
141-
* Contains headers that are considered case-sensitive by default.
142-
*/
143-
public static Set<String> DEFAULT_SENSITIVE = Collections
144-
.unmodifiableSet(new HashSet<>(Arrays.asList("cookie", "authorization")));
145-
146140
private URI uri;
147141

148142
private RestTemplate rest;
@@ -157,7 +151,7 @@ public class ProxyExchange<T> {
157151

158152
private WebDataBinderFactory binderFactory;
159153

160-
private Set<String> sensitive;
154+
private Set<String> excluded;
161155

162156
private HttpHeaders headers = new HttpHeaders();
163157

@@ -210,19 +204,19 @@ public ProxyExchange<T> headers(HttpHeaders headers) {
210204
}
211205

212206
/**
213-
* Sets the names of sensitive headers that are not passed downstream to the backend
207+
* Sets the names of excluded headers that are not passed downstream to the backend
214208
* service.
215-
* @param names the names of sensitive headers
209+
* @param names the names of excluded headers
216210
* @return this for convenience
217211
*/
218-
public ProxyExchange<T> sensitive(String... names) {
219-
if (this.sensitive == null) {
220-
this.sensitive = new HashSet<>();
212+
public ProxyExchange<T> excluded(String... names) {
213+
if (this.excluded == null) {
214+
this.excluded = new HashSet<>();
221215
}
222216

223-
this.sensitive.clear();
217+
this.excluded.clear();
224218
for (String name : names) {
225-
this.sensitive.add(name.toLowerCase());
219+
this.excluded.add(name.toLowerCase());
226220
}
227221
return this;
228222
}
@@ -369,8 +363,8 @@ private Set<String> filterHeaderKeys(HttpHeaders headers) {
369363
}
370364

371365
private Set<String> filterHeaderKeys(Collection<String> headerNames) {
372-
final Set<String> sensitiveHeaders = this.sensitive != null ? this.sensitive : DEFAULT_SENSITIVE;
373-
return headerNames.stream().filter(header -> !sensitiveHeaders.contains(header.toLowerCase()))
366+
final Set<String> excludedHeaders = this.excluded != null ? this.excluded : Collections.emptySet();
367+
return headerNames.stream().filter(header -> !excludedHeaders.contains(header.toLowerCase()))
374368
.collect(Collectors.toSet());
375369
}
376370

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public class ProxyExchangeArgumentResolver implements HandlerMethodArgumentResol
4747

4848
private Set<String> autoForwardedHeaders;
4949

50-
private Set<String> sensitive;
50+
private Set<String> excluded;
5151

5252
public ProxyExchangeArgumentResolver(RestTemplate builder) {
5353
this.rest = builder;
@@ -62,8 +62,8 @@ public void setAutoForwardedHeaders(Set<String> autoForwardedHeaders) {
6262
: autoForwardedHeaders.stream().map(String::toLowerCase).collect(toSet());
6363
}
6464

65-
public void setSensitive(Set<String> sensitive) {
66-
this.sensitive = sensitive;
65+
public void setExcluded(Set<String> excluded) {
66+
this.excluded = excluded;
6767
}
6868

6969
@Override
@@ -77,7 +77,7 @@ public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer m
7777
ProxyExchange<?> proxy = new ProxyExchange<>(rest, webRequest, mavContainer, binderFactory, type(parameter));
7878
configureHeaders(proxy);
7979
configureAutoForwardedHeaders(proxy, webRequest);
80-
configureSensitive(proxy);
80+
configureExcluded(proxy);
8181
return proxy;
8282
}
8383

@@ -115,9 +115,9 @@ private void configureAutoForwardedHeaders(final ProxyExchange<?> proxy, final N
115115
}
116116
}
117117

118-
private void configureSensitive(final ProxyExchange<?> proxy) {
119-
if (sensitive != null) {
120-
proxy.sensitive(sensitive.toArray(new String[0]));
118+
private void configureExcluded(final ProxyExchange<?> proxy) {
119+
if (excluded != null) {
120+
proxy.excluded(excluded.toArray(new String[0]));
121121
}
122122
}
123123

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

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
*
3232
* @author Dave Syer
3333
* @author Tim Ysewyn
34+
* @author Joris Kuipers
3435
*
3536
*/
3637
@ConfigurationProperties("spring.cloud.gateway.proxy")
@@ -42,14 +43,19 @@ public class ProxyProperties {
4243
private Map<String, String> headers = new LinkedHashMap<>();
4344

4445
/**
45-
* A set of header names that should be send downstream by default.
46+
* A set of header names that should be sent downstream by default.
4647
*/
4748
private Set<String> autoForward = new HashSet<>();
4849

4950
/**
5051
* A set of sensitive header names that will not be sent downstream by default.
5152
*/
52-
private Set<String> sensitive = null;
53+
private Set<String> sensitive = Set.of("cookie", "authorization");
54+
55+
/**
56+
* A set of header names that will not be sent downstream because they could be problematic.
57+
*/
58+
private Set<String> skipped = Set.of("content-length", "host");
5359

5460
public Map<String, String> getHeaders() {
5561
return headers;
@@ -75,6 +81,14 @@ public void setSensitive(Set<String> sensitive) {
7581
this.sensitive = sensitive;
7682
}
7783

84+
public Set<String> getSkipped() {
85+
return skipped;
86+
}
87+
88+
public void setSkipped(Set<String> skipped) {
89+
this.skipped = skipped;
90+
}
91+
7892
public HttpHeaders convertHeaders() {
7993
HttpHeaders headers = new HttpHeaders();
8094
for (String key : this.headers.keySet()) {

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717
package org.springframework.cloud.gateway.mvc.config;
1818

1919
import java.io.IOException;
20+
import java.util.HashSet;
2021
import java.util.List;
2122
import java.util.Optional;
23+
import java.util.Set;
2224

2325
import org.springframework.beans.factory.annotation.Autowired;
2426
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
@@ -70,7 +72,14 @@ public boolean supports(Class<?> clazz) {
7072
ProxyExchangeArgumentResolver resolver = new ProxyExchangeArgumentResolver(template);
7173
resolver.setHeaders(proxy.convertHeaders());
7274
resolver.setAutoForwardedHeaders(proxy.getAutoForward());
73-
resolver.setSensitive(proxy.getSensitive()); // can be null
75+
Set<String> excludedHeaderNames = new HashSet<>();
76+
if (proxy.getSensitive() != null) {
77+
excludedHeaderNames.addAll(proxy.getSensitive());
78+
}
79+
if (proxy.getSkipped() != null) {
80+
excludedHeaderNames.addAll(proxy.getSkipped());
81+
}
82+
resolver.setExcluded(excludedHeaderNames);
7483
return resolver;
7584
}
7685

spring-cloud-gateway-mvc/src/test/java/org/springframework/cloud/gateway/mvc/GetWithBodyRequestTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ public ProxyExchangeArgumentResolver proxyExchangeArgumentResolver(final ProxyPr
111111
generateConfiguredRestTemplate());
112112
resolver.setHeaders(proxy.convertHeaders());
113113
resolver.setAutoForwardedHeaders(proxy.getAutoForward());
114-
resolver.setSensitive(proxy.getSensitive());
114+
resolver.setExcluded(proxy.getSensitive());
115115
return resolver;
116116
}
117117

spring-cloud-gateway-mvc/src/test/java/org/springframework/cloud/gateway/mvc/ProductionConfigurationTests.java

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import org.springframework.http.HttpHeaders;
4141
import org.springframework.http.HttpMethod;
4242
import org.springframework.http.HttpStatus;
43+
import org.springframework.http.MediaType;
4344
import org.springframework.http.RequestEntity;
4445
import org.springframework.http.ResponseEntity;
4546
import org.springframework.http.client.SimpleClientHttpRequestFactory;
@@ -56,8 +57,7 @@
5657

5758
import static org.assertj.core.api.Assertions.assertThat;
5859

59-
@SpringBootTest(properties = { "spring.cloud.gateway.proxy.auto-forward=Baz" },
60-
webEnvironment = WebEnvironment.RANDOM_PORT)
60+
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
6161
@ContextConfiguration(classes = TestApplication.class)
6262
public class ProductionConfigurationTests {
6363

@@ -112,6 +112,21 @@ public void post() {
112112
.isEqualTo("host=localhost:" + port + ";foo");
113113
}
114114

115+
@Test
116+
public void postJsonWithWhitespace() {
117+
var json = """
118+
{
119+
"foo": "bar"
120+
}""";
121+
122+
var headers = new HttpHeaders();
123+
headers.setContentType(MediaType.APPLICATION_JSON);
124+
headers.setContentLength(json.length());
125+
var request = new HttpEntity<>(json, headers);
126+
assertThat(rest.postForEntity("/proxy/checkContentLength", request, Void.class).getStatusCode())
127+
.isEqualTo(HttpStatus.OK);
128+
}
129+
115130
@Test
116131
public void forward() {
117132
assertThat(rest.getForObject("/forward/foos/0", Foo.class).getName()).isEqualTo("bye");
@@ -424,7 +439,7 @@ public void postForwardForgetBody(@RequestBody byte[] body, ProxyExchange<?> pro
424439
@GetMapping("/proxy/headers")
425440
@SuppressWarnings("Duplicates")
426441
public ResponseEntity<Map<String, List<String>>> headers(ProxyExchange<Map<String, List<String>>> proxy) {
427-
proxy.sensitive("foo", "hello");
442+
proxy.excluded("foo", "hello");
428443
proxy.header("bar", "hello");
429444
proxy.header("abc", "123");
430445
proxy.header("hello", "world");
@@ -440,6 +455,12 @@ public ResponseEntity<Map<String, List<String>>> defaultSensitiveHeaders(
440455
return proxy.uri(home.toString() + "/headers").get();
441456
}
442457

458+
@PostMapping("/proxy/checkContentLength")
459+
public ResponseEntity<?> checkContentLength(
460+
ProxyExchange<byte[]> proxy) {
461+
return proxy.uri(home.toString() + "/checkContentLength").post();
462+
}
463+
443464
private <T> ResponseEntity<T> first(ResponseEntity<List<T>> response) {
444465
return ResponseEntity.status(response.getStatusCode()).headers(response.getHeaders())
445466
.body(response.getBody().iterator().next());
@@ -484,6 +505,15 @@ public List<Bar> bars(@RequestBody List<Foo> foos, @RequestHeader HttpHeaders he
484505
return Arrays.asList(new Bar(custom + foos.iterator().next().getName()));
485506
}
486507

508+
@PostMapping("/checkContentLength")
509+
public ResponseEntity<?> checkContentLength(@RequestHeader(name = "Content-Length", required = false) Integer contentLength,
510+
@RequestBody String json) {
511+
if (contentLength != null && contentLength != json.length()) {
512+
return ResponseEntity.badRequest().build();
513+
}
514+
return ResponseEntity.ok().build();
515+
}
516+
487517
@GetMapping("/headers")
488518
public Map<String, List<String>> headers(@RequestHeader HttpHeaders headers) {
489519
return new LinkedMultiValueMap<>(headers);

spring-cloud-gateway-webflux/src/main/java/org/springframework/cloud/gateway/webflux/ProxyExchange.java

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,11 @@
6464
*
6565
* <p>
6666
* By default the incoming request body and headers are sent intact to the downstream
67-
* service (with the exception of "sensitive" headers). To manipulate the downstream
67+
* service (with the exception of "excluded" headers). To manipulate the downstream
6868
* request there are "builder" style methods in {@link ProxyExchange}, but only the
69-
* {@link #uri(String)} is mandatory. You can change the sensitive headers by calling the
70-
* {@link #sensitive(String...)} method (Authorization and Cookie are sensitive by
71-
* default).
69+
* {@link #uri(String)} is mandatory. You can change the excluded headers by calling the
70+
* {@link #excluded(String...)} method (the argument resolver will populate these with
71+
* some sensible defaults).
7272
* </p>
7373
* <p>
7474
* The type parameter <code>T</code> in <code>ProxyExchange&lt;T&gt;</code> is the type of
@@ -111,12 +111,6 @@
111111
*/
112112
public class ProxyExchange<T> {
113113

114-
/**
115-
* Contains headers that are considered case-sensitive by default.
116-
*/
117-
public static Set<String> DEFAULT_SENSITIVE = Collections
118-
.unmodifiableSet(new HashSet<>(Arrays.asList("cookie", "authorization")));
119-
120114
private HttpMethod httpMethod;
121115

122116
private URI uri;
@@ -131,7 +125,7 @@ public class ProxyExchange<T> {
131125

132126
private BindingContext bindingContext;
133127

134-
private Set<String> sensitive;
128+
private Set<String> excluded;
135129

136130
private HttpHeaders headers = new HttpHeaders();
137131

@@ -197,19 +191,19 @@ public ProxyExchange<T> headers(HttpHeaders headers) {
197191
}
198192

199193
/**
200-
* Sets the names of sensitive headers that are not passed downstream to the backend
194+
* Sets the names of excluded headers that are not passed downstream to the backend
201195
* service.
202-
* @param names the names of sensitive headers
196+
* @param names the names of excluded headers
203197
* @return this for convenience
204198
*/
205-
public ProxyExchange<T> sensitive(String... names) {
206-
if (this.sensitive == null) {
207-
this.sensitive = new HashSet<>();
199+
public ProxyExchange<T> excluded(String... names) {
200+
if (this.excluded == null) {
201+
this.excluded = new HashSet<>();
208202
}
209203

210-
this.sensitive.clear();
204+
this.excluded.clear();
211205
for (String name : names) {
212-
this.sensitive.add(name.toLowerCase());
206+
this.excluded.add(name.toLowerCase());
213207
}
214208
return this;
215209
}
@@ -389,8 +383,8 @@ private void addHeaders(HttpHeaders headers, HttpHeaders toAdd) {
389383
}
390384

391385
private Set<String> filterHeaderKeys(HttpHeaders headers) {
392-
final Set<String> sensitiveHeaders = this.sensitive != null ? this.sensitive : DEFAULT_SENSITIVE;
393-
return headers.keySet().stream().filter(header -> !sensitiveHeaders.contains(header.toLowerCase()))
386+
final Set<String> excludedHeaders = this.excluded != null ? this.excluded : Collections.emptySet();
387+
return headers.keySet().stream().filter(header -> !excludedHeaders.contains(header.toLowerCase()))
394388
.collect(Collectors.toSet());
395389
}
396390

spring-cloud-gateway-webflux/src/main/java/org/springframework/cloud/gateway/webflux/config/ProxyExchangeArgumentResolver.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public class ProxyExchangeArgumentResolver implements HandlerMethodArgumentResol
4444

4545
private Set<String> autoForwardedHeaders;
4646

47-
private Set<String> sensitive;
47+
private Set<String> excluded;
4848

4949
public ProxyExchangeArgumentResolver(WebClient builder) {
5050
this.rest = builder;
@@ -58,8 +58,8 @@ public void setAutoForwardedHeaders(Set<String> autoForwardedHeaders) {
5858
this.autoForwardedHeaders = autoForwardedHeaders;
5959
}
6060

61-
public void setSensitive(Set<String> sensitive) {
62-
this.sensitive = sensitive;
61+
public void setExcluded(Set<String> excluded) {
62+
this.excluded = excluded;
6363
}
6464

6565
@Override
@@ -87,8 +87,8 @@ public Mono<Object> resolveArgument(MethodParameter parameter, BindingContext bi
8787
if (this.autoForwardedHeaders.size() > 0) {
8888
proxy.headers(extractAutoForwardedHeaders(exchange));
8989
}
90-
if (sensitive != null) {
91-
proxy.sensitive(sensitive.toArray(new String[0]));
90+
if (excluded != null) {
91+
proxy.excluded(excluded.toArray(new String[0]));
9292
}
9393
return Mono.just(proxy);
9494
}

0 commit comments

Comments
 (0)