Skip to content

Recommend disabling context path redirects when using proxy-terminated SSL with Tomcat #22908

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
TheSmoun opened this issue Aug 11, 2020 · 10 comments
Assignees
Labels
type: documentation A documentation update
Milestone

Comments

@TheSmoun
Copy link

Affects: Spring Boot v2.2.1.RELEASE


context path redirect causes protocol downgrade to http

I'm trying to run a basic Spring Boot application behind a proxy and with a context path, because later it will run on a server with other applications and inside a Docker container.
I'm having difficulties with the redirect to the context path. Here is a structural overview of the system:

+--------+  https  +-------+  http   +--------+
| Client | ------> | Proxy | ------> | Spring |
+--------+         +-------+         +--------+

When sending a request with the schema https://<proxy-url>/<context-path>, the Spring redirects the client to http://<proxy-url>/<context-path>/. So it downgrades the protocol from https to http.

I honestly don't know whether this is a bug or intended behavior. Is there a way to make Spring redirect to https instead of http. The proxy redirect contains the correct X-Forwarded-Proto, X-Forwarded-Host, X-Forwarded-Port headers.

The Curl output below shows that Spring reads the headers properly when adding the slash at the end of the context path. The Curl requests talk directly to Spring and I did set the headers manually to emulate the proxy. I also attached the code of the test application and a screenshot of the redirect within Chrome.

Request and redirect screenshot

redirect

Curl

Slash at the end of the context path

C:\Users\<user>>curl -v -H "X-Forwarded-Proto: https" -H "X-Forwarded-Port: 443" -H "X-Forwarded-Host: <host>" http://localhost:8081/hweproxy/
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8081 (#0)
> GET /hweproxy/ HTTP/1.1
> Host: localhost:8081
> User-Agent: curl/7.55.1
> Accept: */*
> X-Forwarded-Proto: https
> X-Forwarded-Port: 443
> X-Forwarded-Host: <host>
>
< HTTP/1.1 200
< Vary: Origin
< Vary: Access-Control-Request-Method
< Vary: Access-Control-Request-Headers
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 1; mode=block
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< Strict-Transport-Security: max-age=31536000 ; includeSubDomains
< X-Frame-Options: DENY
< Content-Type: text/plain;charset=UTF-8
< Content-Length: 33
< Date: Tue, 11 Aug 2020 09:32:52 GMT
<
https://<host>/hweproxy/* Connection #0 to host localhost left intact

No slash at the end of the context path

C:\Users\<user>>curl -v -H "X-Forwarded-Proto: https" -H "X-Forwarded-Port: 443" -H "X-Forwarded-Host: <host>" http://localhost:8081/hweproxy
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8081 (#0)
> GET /hweproxy HTTP/1.1
> Host: localhost:8081
> User-Agent: curl/7.55.1
> Accept: */*
> X-Forwarded-Proto: https
> X-Forwarded-Port: 443
> X-Forwarded-Host: <host>
>
< HTTP/1.1 302
< Location: http://localhost:8081/hweproxy/
< Transfer-Encoding: chunked
< Date: Tue, 11 Aug 2020 09:19:51 GMT
<
* Connection #0 to host localhost left intact

Basic Spring Boot application code

Java Code

@SpringBootApplication
public class SpringPoC {

    public static void main(String[] args) {
        SpringApplication.run(SpringPoC.class, args);
    }
}

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("**").permitAll()

            .and()
            .cors()

            .and()
            .csrf()
            .disable();
    }
}

@RestController
@RequestMapping("/")
public class DummyController {
    
    @GetMapping
    public ResponseEntity<?> dummyEndpoint(HttpServletRequest req) {
        return ResponseEntity.ok(req.getRequestURL());
    }
}

application.properties

server.port = 8081
server.servlet.context-path = /hweproxy
server.forward-headers-strategy = FRAMEWORK
server.tomcat.remoteip.internal-proxies = \\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}

pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>spring-poc</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>Spring Prove of Concept</name>
    <url>http://example.com</url>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.1.RELEASE</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>

    <build>
        <sourceDirectory>src/main/java</sourceDirectory>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
            </resource>
        </resources>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>11</source>
                    <target>11</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <includeSystemScope>true</includeSystemScope>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
    </dependencies>
</project>
@jnizet
Copy link
Contributor

jnizet commented Aug 12, 2020

My guess is that Spring is not involved here, and that Tomcat is the one doing the redirect. Have you tried using native rather than framework for the server.forward-headers-strategy?

@TheSmoun
Copy link
Author

NATIVE did show the same results:

curl -v -H "X-Forwarded-Proto: https" -H "X-Forwarded-Port: 443" -H "X-Forwarded-Host: <host>" http://localhost:8081/hweproxy
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8081 (#0)
> GET /hweproxy HTTP/1.1
> Host: localhost:8081
> User-Agent: curl/7.55.1
> Accept: */*
> X-Forwarded-Proto: https
> X-Forwarded-Port: 443
> X-Forwarded-Host: <host>
>
< HTTP/1.1 302
< Location: http://localhost:8081/hweproxy/
< Transfer-Encoding: chunked
< Date: Wed, 12 Aug 2020 08:33:54 GMT
<
* Connection #0 to host localhost left intact
curl -v -H "X-Forwarded-Proto: https" -H "X-Forwarded-Port: 443" -H "X-Forwarded-Host: <host>" http://localhost:8081/hweproxy/
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8081 (#0)
> GET /hweproxy/ HTTP/1.1
> Host: localhost:8081
> User-Agent: curl/7.55.1
> Accept: */*
> X-Forwarded-Proto: https
> X-Forwarded-Port: 443
> X-Forwarded-Host: <host>
>
< HTTP/1.1 200
< Vary: Origin
< Vary: Access-Control-Request-Method
< Vary: Access-Control-Request-Headers
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 1; mode=block
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< Strict-Transport-Security: max-age=31536000 ; includeSubDomains
< X-Frame-Options: DENY
< Content-Type: text/plain;charset=UTF-8
< Content-Length: 33
< Date: Wed, 12 Aug 2020 08:34:07 GMT
<
https://<host>/hweproxy/* Connection #0 to host localhost left intact

@TheSmoun
Copy link
Author

We did some more research.
There are two properties that control the redirect to the context path of the underlying Tomcat server. The first one is server.tomcat.redirect-context-root which controls whether a redirect should happen and the second one is server.tomcat.use-relative-redirects=true which controls whether the redirect should be relative or absolute. Setting it to relative works as a workaround, but the core issue remains.

Tomcat completely ignores properties like server.tomcat.remoteip.protocol-header = X-Forwarded-Proto for the redirect to the context path. When accessing the URL internally, this works fine. There are several configuration properties for Tomcat which lets you control this, like scheme, proxyPort and proxyName, but none of them can be configured through Spring, right?

All in all this sounds like a bug in the Tomcat configuration Spring performs.

@sbrannen sbrannen changed the title context path redirect causes protocol downgrade to http Context path redirect causes protocol downgrade from HTTPS to HTTP in embedded Tomcat Aug 12, 2020
@sbrannen
Copy link
Member

@philwebb, @wilkinsona, can one of you please transfer this to Spring Boot's issue tracker?

@snicoll snicoll transferred this issue from spring-projects/spring-framework Aug 12, 2020
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Aug 12, 2020
@wilkinsona
Copy link
Member

CoyoteAdapter triggers the redirect before passing the request through the valve pipeline. As a result RemoteIpValve doesn't get an opportunity to honour the X-Forwarded-Proto header. I'm not sure that there's much we can do about that as it's Tomcat's standard behaviour. @markt-asf, is it possible to have mapper-triggered redirect honour an X-Forwarded-Proto header?

Disabling the redirect should be sufficient to get this to work as you wish.

server.port = 8081
server.servlet.context-path = /hweproxy
server.forward-headers-strategy = native
server.tomcat.redirect-context-root = false

The application.properties above result in your dummy controller being reached with an https request URL:

curl -v -H "X-Forwarded-Proto: https" -H "X-Forwarded-Port: 443" -H "X-Forwarded-Host: <host>" http://localhost:8081/hweproxy

*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8081 (#0)
> GET /hweproxy HTTP/1.1
> Host: localhost:8081
> User-Agent: curl/7.64.1
> Accept: */*
> X-Forwarded-Proto: https
> X-Forwarded-Port: 443
> X-Forwarded-Host: <host>
> 
< HTTP/1.1 200 
< Content-Type: text/plain;charset=UTF-8
< Content-Length: 26
< Date: Wed, 12 Aug 2020 12:40:06 GMT
< 
* Connection #0 to host localhost left intact
https://localhost/hweproxy*

Related to this, a piece of your original configuration doesn't make sense in combination:

server.forward-headers-strategy = FRAMEWORK
server.tomcat.remoteip.internal-proxies = \\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}

If you're using the Framework's forward headers strategy, Tomcat's remote IP valve will not be involved and, therefore, configuring its internal proxies will have no effect.

@markt-asf
Copy link

Anything is possible but it isn't practical / realistic / performant for the Mapper to take account of a X-Forwarded-Proto header.
I think all the possible options have been discussed above. I'd use the Boot equivalent of mapperContextRootRedirectEnabled="false" on the Context.

@wilkinsona
Copy link
Member

Thanks, Mark. I think we should make this a documentation issue and recommend setting server.tomcat.redirect-context-root=false when using HTTPS and the native forward headers strategy.

@wilkinsona wilkinsona changed the title Context path redirect causes protocol downgrade from HTTPS to HTTP in embedded Tomcat Recommend disabling context path redirects when using HTTPS and the native forward headers strategy with Tomcat Aug 15, 2020
@wilkinsona wilkinsona added type: documentation A documentation update and removed status: waiting-for-triage An issue we've not yet triaged labels Aug 15, 2020
@wilkinsona wilkinsona added this to the 2.2.x milestone Aug 15, 2020
@bigunyak
Copy link

Oh, that default server.tomcat.redirect-context-root=true config setting costed me a full day of battling trying to understand why the x-forwarded- headers don't work no matter what I have the server.forward-headers-strategy setting set to, until finally found this issue here.
My problem is that I have two proxies in front of my Spring application and even thought x-forwarded-host is correctly pointing to the host1 and server.forward-headers-strategy is set to either native or framework, my requests to the context root path would still end up redirected to the host2.

                host1         host2
+--------+    +--------+    +--------+    +--------+
| Client +--->+ Proxy1 +--->+ Proxy2 +--->+ Spring |
+--------+    +--------+    +--------+    +--------+

Such disrespect of x-forwarded- headers for the context-path feels very much like a bug in Tomcat to me. How could it not work for context-path and work in other cases?
But could at least this shortcoming be well documented in Spring docs, or even consider changing default value for server.tomcat.redirect-context-root to false?

@wilkinsona wilkinsona changed the title Recommend disabling context path redirects when using HTTPS and the native forward headers strategy with Tomcat Recommend disabling context path redirects when using proxy-terminated SSL with Tomcat Nov 4, 2020
@wilkinsona wilkinsona self-assigned this Nov 4, 2020
@wilkinsona wilkinsona modified the milestones: 2.2.x, 2.2.12 Nov 4, 2020
@kaus02

This comment has been minimized.

@snicoll

This comment has been minimized.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: documentation A documentation update
Projects
None yet
Development

No branches or pull requests

9 participants