Skip to content

Commit 10f5926

Browse files
authored
Merge pull request #44184 from cescoffier/x-forwarded-trusted-proxy-header
Add Support for Trusted Proxy Detection on Forwarded Requests
2 parents a6d6cd7 + 5069761 commit 10f5926

File tree

7 files changed

+230
-9
lines changed

7 files changed

+230
-9
lines changed

extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/ForwardedHandlerInitializer.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,17 @@ public void register(@Observes Router router) {
1919
+ "|" + rc.request().remoteAddress().toString()
2020
+ "|" + rc.request().uri()
2121
+ "|" + rc.request().absoluteURI()));
22+
router.route("/trusted-proxy").handler(rc -> rc.response()
23+
.end(rc.request().scheme() + "|" + rc.request().getHeader(HttpHeaders.HOST) + "|"
24+
+ rc.request().remoteAddress().toString()
25+
+ "|" + rc.request().getHeader("X-Forwarded-Trusted-Proxy")));
26+
router.route("/path-trusted-proxy").handler(rc -> rc.response()
27+
.end(rc.request().scheme()
28+
+ "|" + rc.request().getHeader(HttpHeaders.HOST)
29+
+ "|" + rc.request().remoteAddress().toString()
30+
+ "|" + rc.request().uri()
31+
+ "|" + rc.request().absoluteURI()
32+
+ "|" + rc.request().getHeader("X-Forwarded-Trusted-Proxy")));
2233
}
2334

2435
}

extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/ForwardedHeaderTest.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,41 @@ public void test() {
3030
.body(Matchers.equalTo("https|somehost|backend:4444"));
3131
}
3232

33+
@Test
34+
public void testWithoutTrustedProxyHeader() {
35+
assertThat(RestAssured.get("/forward").asString()).startsWith("http|");
36+
RestAssured.given()
37+
.header("Forwarded", "by=proxy;for=backend:4444;host=somehost;proto=https")
38+
.get("/trusted-proxy")
39+
.then()
40+
.body(Matchers.equalTo("https|somehost|backend:4444|null"));
41+
}
42+
43+
@Test
44+
public void testThatTrustedProxyHeaderCannotBeForged() {
45+
assertThat(RestAssured.get("/forward").asString()).startsWith("http|");
46+
RestAssured.given()
47+
.header("Forwarded", "by=proxy;for=backend:4444;host=somehost;proto=https")
48+
.header("X-Forwarded-Trusted-Proxy", "true")
49+
.get("/trusted-proxy")
50+
.then()
51+
.body(Matchers.equalTo("https|somehost|backend:4444|null"));
52+
53+
RestAssured.given()
54+
.header("Forwarded", "by=proxy;for=backend:4444;host=somehost;proto=https")
55+
.header("X-Forwarded-Trusted-Proxy", "hello")
56+
.get("/trusted-proxy")
57+
.then()
58+
.body(Matchers.equalTo("https|somehost|backend:4444|null"));
59+
60+
RestAssured.given()
61+
.header("Forwarded", "by=proxy;for=backend:4444;host=somehost;proto=https")
62+
.header("X-Forwarded-Trusted-Proxy", "false")
63+
.get("/trusted-proxy")
64+
.then()
65+
.body(Matchers.equalTo("https|somehost|backend:4444|null"));
66+
}
67+
3368
@Test
3469
public void testForwardedForWithSequenceOfProxies() {
3570
assertThat(RestAssured.get("/forward").asString()).startsWith("http|");

extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/proxy/TrustedForwarderProxyTest.java

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package io.quarkus.vertx.http.proxy;
22

3+
import static org.assertj.core.api.Assertions.assertThat;
4+
35
import org.hamcrest.Matchers;
46
import org.jboss.shrinkwrap.api.asset.StringAsset;
57
import org.junit.jupiter.api.Test;
@@ -31,6 +33,51 @@ public void testHeadersAreUsed() {
3133
.body(Matchers.equalTo("http|somehost2|backend2:5555|/path|http://somehost2/path"));
3234
}
3335

36+
@Test
37+
public void testHeadersAreUsedWithTrustedProxyHeader() {
38+
RestAssured.given()
39+
.header("Forwarded", "proto=http;for=backend2:5555;host=somehost2")
40+
.get("/path-trusted-proxy")
41+
.then()
42+
.body(Matchers
43+
.equalTo("http|somehost2|backend2:5555|/path-trusted-proxy|http://somehost2/path-trusted-proxy|null"));
44+
}
45+
46+
@Test
47+
public void testWithoutTrustedProxyHeader() {
48+
assertThat(RestAssured.get("/forward").asString()).startsWith("http|");
49+
RestAssured.given()
50+
.header("Forwarded", "by=proxy;for=backend:4444;host=somehost;proto=https")
51+
.get("/trusted-proxy")
52+
.then()
53+
.body(Matchers.equalTo("https|somehost|backend:4444|null"));
54+
}
55+
56+
@Test
57+
public void testThatTrustedProxyHeaderCannotBeForged() {
58+
assertThat(RestAssured.get("/forward").asString()).startsWith("http|");
59+
RestAssured.given()
60+
.header("Forwarded", "by=proxy;for=backend:4444;host=somehost;proto=https")
61+
.header("X-Forwarded-Trusted-Proxy", "true")
62+
.get("/trusted-proxy")
63+
.then()
64+
.body(Matchers.equalTo("https|somehost|backend:4444|null"));
65+
66+
RestAssured.given()
67+
.header("Forwarded", "by=proxy;for=backend:4444;host=somehost;proto=https")
68+
.header("X-Forwarded-Trusted-Proxy", "hello")
69+
.get("/trusted-proxy")
70+
.then()
71+
.body(Matchers.equalTo("https|somehost|backend:4444|null"));
72+
73+
RestAssured.given()
74+
.header("Forwarded", "by=proxy;for=backend:4444;host=somehost;proto=https")
75+
.header("X-Forwarded-Trusted-Proxy", "false")
76+
.get("/trusted-proxy")
77+
.then()
78+
.body(Matchers.equalTo("https|somehost|backend:4444|null"));
79+
}
80+
3481
/**
3582
* As described on <a href=
3683
* "https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forwarded">https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forwarded</a>,
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package io.quarkus.vertx.http.proxy;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import org.hamcrest.Matchers;
6+
import org.jboss.shrinkwrap.api.asset.StringAsset;
7+
import org.junit.jupiter.api.Test;
8+
import org.junit.jupiter.api.extension.RegisterExtension;
9+
10+
import io.quarkus.test.QuarkusUnitTest;
11+
import io.quarkus.vertx.http.ForwardedHandlerInitializer;
12+
import io.restassured.RestAssured;
13+
14+
/**
15+
* Test the trusted-proxy header
16+
*/
17+
public class TrustedProxyHeaderTest {
18+
19+
@RegisterExtension
20+
static final QuarkusUnitTest config = new QuarkusUnitTest()
21+
.withApplicationRoot((jar) -> jar
22+
.addClasses(ForwardedHandlerInitializer.class)
23+
.addAsResource(new StringAsset("""
24+
quarkus.http.proxy.proxy-address-forwarding=true
25+
quarkus.http.proxy.allow-forwarded=true
26+
quarkus.http.proxy.enable-forwarded-host=true
27+
quarkus.http.proxy.enable-forwarded-prefix=true
28+
quarkus.http.proxy.allow-forwarded=true
29+
quarkus.http.proxy.enable-trusted-proxy-header=true
30+
quarkus.http.proxy.trusted-proxies=localhost
31+
"""),
32+
"application.properties"));
33+
34+
@Test
35+
public void testHeadersAreUsed() {
36+
RestAssured.given()
37+
.header("Forwarded", "proto=http;for=backend2:5555;host=somehost2")
38+
.get("/path-trusted-proxy")
39+
.then()
40+
.body(Matchers
41+
.equalTo("http|somehost2|backend2:5555|/path-trusted-proxy|http://somehost2/path-trusted-proxy|true"));
42+
}
43+
44+
@Test
45+
public void testTrustedProxyHeader() {
46+
assertThat(RestAssured.get("/forward").asString()).startsWith("http|");
47+
RestAssured.given()
48+
.header("Forwarded", "by=proxy;for=backend:4444;host=somehost;proto=https")
49+
.get("/trusted-proxy")
50+
.then()
51+
.body(Matchers.equalTo("https|somehost|backend:4444|true"));
52+
}
53+
54+
@Test
55+
public void testThatTrustedProxyHeaderCannotBeForged() {
56+
assertThat(RestAssured.get("/forward").asString()).startsWith("http|");
57+
RestAssured.given()
58+
.header("Forwarded", "by=proxy;for=backend:4444;host=somehost;proto=https")
59+
.header("X-Forwarded-Trusted-Proxy", "true")
60+
.get("/trusted-proxy")
61+
.then()
62+
.body(Matchers.equalTo("https|somehost|backend:4444|true"));
63+
64+
RestAssured.given()
65+
.header("Forwarded", "by=proxy;for=backend:4444;host=somehost;proto=https")
66+
.header("X-Forwarded-Trusted-Proxy", "hello")
67+
.get("/trusted-proxy")
68+
.then()
69+
.body(Matchers.equalTo("https|somehost|backend:4444|true"));
70+
71+
RestAssured.given()
72+
.header("Forwarded", "by=proxy;for=backend:4444;host=somehost;proto=https")
73+
.header("X-Forwarded-Trusted-Proxy", "false")
74+
.get("/trusted-proxy")
75+
.then()
76+
.body(Matchers.equalTo("https|somehost|backend:4444|true"));
77+
}
78+
79+
/**
80+
* As described on <a href=
81+
* "https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forwarded">https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forwarded</a>,
82+
* the syntax should be case-insensitive.
83+
* <p>
84+
* Kong, for example, uses `Proto` instead of `proto` and `For` instead of `for`.
85+
*/
86+
@Test
87+
public void testHeadersAreUsedWhenUsingCasedCharacters() {
88+
RestAssured.given()
89+
.header("Forwarded", "Proto=http;For=backend2:5555;Host=somehost2")
90+
.get("/path-trusted-proxy")
91+
.then()
92+
.body(Matchers
93+
.equalTo("http|somehost2|backend2:5555|/path-trusted-proxy|http://somehost2/path-trusted-proxy|true"));
94+
}
95+
}

extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardedParser.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ class ForwardedParser {
4040
private static final AsciiString X_FORWARDED_PROTO = AsciiString.cached("X-Forwarded-Proto");
4141
private static final AsciiString X_FORWARDED_PORT = AsciiString.cached("X-Forwarded-Port");
4242
private static final AsciiString X_FORWARDED_FOR = AsciiString.cached("X-Forwarded-For");
43+
private static final AsciiString X_FORWARDED_TRUSTED_PROXY = AsciiString.cached("X-Forwarded-Trusted-Proxy");
4344

4445
private static final Pattern FORWARDED_HOST_PATTERN = Pattern.compile("host=\"?([^;,\"]+)\"?", Pattern.CASE_INSENSITIVE);
4546
private static final Pattern FORWARDED_PROTO_PATTERN = Pattern.compile("proto=\"?([^;,\"]+)\"?", Pattern.CASE_INSENSITIVE);
@@ -128,7 +129,8 @@ private void calculate() {
128129
setHostAndPort(delegate.host(), port);
129130
uri = delegate.uri();
130131

131-
if (trustedProxyCheck.isProxyAllowed()) {
132+
boolean isProxyAllowed = trustedProxyCheck.isProxyAllowed();
133+
if (isProxyAllowed) {
132134
String forwarded = delegate.getHeader(FORWARDED);
133135
if (forwardingProxyOptions.allowForwarded && forwarded != null) {
134136
Matcher matcher = FORWARDED_PROTO_PATTERN.matcher(forwarded);
@@ -193,6 +195,21 @@ private void calculate() {
193195
authority = HostAndPort.create(host, port >= 0 ? port : -1);
194196
host = host + (port >= 0 ? ":" + port : "");
195197
delegate.headers().set(HOST_HEADER, host);
198+
// TODO Add a test
199+
if (forwardingProxyOptions.enableTrustedProxyHeader) {
200+
// Verify that the header was not already set.
201+
if (delegate.headers().contains(X_FORWARDED_TRUSTED_PROXY)) {
202+
log.warn("The header " + X_FORWARDED_TRUSTED_PROXY + " was already set. Overwriting it.");
203+
}
204+
delegate.headers().set(X_FORWARDED_TRUSTED_PROXY, Boolean.toString(isProxyAllowed));
205+
} else {
206+
// Verify that the header was not already set - to avoid forgery.
207+
if (delegate.headers().contains(X_FORWARDED_TRUSTED_PROXY)) {
208+
log.warn("The header " + X_FORWARDED_TRUSTED_PROXY + " was already set. Removing it.");
209+
delegate.headers().remove(X_FORWARDED_TRUSTED_PROXY);
210+
}
211+
}
212+
196213
absoluteURI = scheme + "://" + host + uri;
197214
log.debug("Recalculated absoluteURI to " + absoluteURI);
198215
}

extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardingProxyOptions.java

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,16 @@ public class ForwardingProxyOptions {
1515
final AsciiString forwardedHostHeader;
1616
final AsciiString forwardedPrefixHeader;
1717
public final TrustedProxyCheckBuilder trustedProxyCheckBuilder;
18+
final boolean enableTrustedProxyHeader;
1819

1920
public ForwardingProxyOptions(final boolean proxyAddressForwarding,
20-
final boolean allowForwarded,
21-
final boolean allowXForwarded,
22-
final boolean enableForwardedHost,
23-
final AsciiString forwardedHostHeader,
24-
final boolean enableForwardedPrefix,
25-
final AsciiString forwardedPrefixHeader,
21+
boolean allowForwarded,
22+
boolean allowXForwarded,
23+
boolean enableForwardedHost,
24+
boolean enableTrustedProxyHeader,
25+
AsciiString forwardedHostHeader,
26+
boolean enableForwardedPrefix,
27+
AsciiString forwardedPrefixHeader,
2628
TrustedProxyCheckBuilder trustedProxyCheckBuilder) {
2729
this.proxyAddressForwarding = proxyAddressForwarding;
2830
this.allowForwarded = allowForwarded;
@@ -32,15 +34,16 @@ public ForwardingProxyOptions(final boolean proxyAddressForwarding,
3234
this.forwardedHostHeader = forwardedHostHeader;
3335
this.forwardedPrefixHeader = forwardedPrefixHeader;
3436
this.trustedProxyCheckBuilder = trustedProxyCheckBuilder;
37+
this.enableTrustedProxyHeader = enableTrustedProxyHeader;
3538
}
3639

3740
public static ForwardingProxyOptions from(ProxyConfig proxy) {
3841
final boolean proxyAddressForwarding = proxy.proxyAddressForwarding;
3942
final boolean allowForwarded = proxy.allowForwarded;
4043
final boolean allowXForwarded = proxy.allowXForwarded.orElse(!allowForwarded);
41-
4244
final boolean enableForwardedHost = proxy.enableForwardedHost;
4345
final boolean enableForwardedPrefix = proxy.enableForwardedPrefix;
46+
final boolean enableTrustedProxyHeader = proxy.enableTrustedProxyHeader;
4447
final AsciiString forwardedPrefixHeader = AsciiString.cached(proxy.forwardedPrefixHeader);
4548
final AsciiString forwardedHostHeader = AsciiString.cached(proxy.forwardedHostHeader);
4649

@@ -50,6 +53,7 @@ public static ForwardingProxyOptions from(ProxyConfig proxy) {
5053
|| parts.isEmpty() ? null : TrustedProxyCheckBuilder.builder(parts);
5154

5255
return new ForwardingProxyOptions(proxyAddressForwarding, allowForwarded, allowXForwarded, enableForwardedHost,
53-
forwardedHostHeader, enableForwardedPrefix, forwardedPrefixHeader, proxyCheckBuilder);
56+
enableTrustedProxyHeader, forwardedHostHeader, enableForwardedPrefix, forwardedPrefixHeader,
57+
proxyCheckBuilder);
5458
}
5559
}

extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ProxyConfig.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,18 @@ public class ProxyConfig {
7676
@ConfigItem(defaultValue = "X-Forwarded-Prefix")
7777
public String forwardedPrefixHeader;
7878

79+
/**
80+
* Adds the header `X-Forwarded-Trusted-Proxy` if the request is forwarded by a trusted proxy.
81+
* The value is `true` if the request is forwarded by a trusted proxy, otherwise `null`.
82+
* <p>
83+
* The forwarded parser detects forgery attempts and if the incoming request contains this header, it will be removed
84+
* from the request.
85+
* <p>
86+
* The `X-Forwarded-Trusted-Proxy` header is a custom header, not part of the standard `Forwarded` header.
87+
*/
88+
@ConfigItem(defaultValue = "false")
89+
public boolean enableTrustedProxyHeader;
90+
7991
/**
8092
* Configure the list of trusted proxy addresses.
8193
* Received `Forwarded`, `X-Forwarded` or `X-Forwarded-*` headers from any other proxy address will be ignored.

0 commit comments

Comments
 (0)