Fixing Spring CORS behind bad proxies

Categories: Java, Cloud

Overview

The Java Spring framework includes a “security” module which adds support for validating http requests in various ways. Among the many features it includes is optional support for CORS (Cross-Origin Request Sharing) requests.

When the spring-based application is running behind a proxy which terminates HTTPS and forwards requests to the app as plain HTTP, then Spring 4.3.12 CORS suport requires the proxy to add the following http headers:

  • X-Forwarded-Proto
  • X-Forwarded-Port

There are unfortunately at least two widely-used proxies which fail to add the X-Forwarded-Port header to requests (in current versions):

  • Google Identity Aware Proxy (IAP)
  • Traefik

The result is that CORS requests fail when they should succeed. To make the situation worse, the Chrome browser sets the “origin” http header on many requests which are technically not CORS requests, but Spring CORS support always runs its logic against any request with an origin header - and thus such requests fail when the user is using Chrome.

Spring issue SPR-16262 is currently tracking this issue. Note however that it isn’t really a Spring bug - spring-web CORS support does work fine when the proxy in front of the app sets the appropriate headers. And it isn’t really a Chrome bug - setting the origin header is not unreasonable. It could possibly be called a bug in IAP/Traefik - but it is not surprising that the implementers of those proxies are not aware of the implications of the failing header. Really, it is just an unfortunate chain of circumstances.

What is more unfortunate is that the cause of these failing requests is really difficult to determine. To those reading this article: I hope you found this before spending too much time tracking the problem down!

Solving the Problem

Hopefully, a future release of Spring will somehow deal with the problem better - and that future releases of IAP/Traefik set the headers.

In the meantime, here is a workaround:

// Copyright Simon Kitching 2017. Available under the Apache License 2.0
package net.vonos.spring.security;

import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.IOException;
import java.util.Collections;
import java.util.Enumeration;

/**
 * Spring context configuration class which adds a servlet-filter to the servlet-engine filter chain in order to add http-header X-Forwarded-Port
 * when needed.
 * <p>
 * When an incoming HTTPS connection is intercepted by an external loadbalancer (or similar), the https connection terminated there, and the
 * request forwarded to this app as HTTP, then the intercepting proxy should set headers X-Forwarded-Proto=https and X-Forwarded-Port=443. Various
 * code may depend on these headers - and in particular, checks done by the spring CorsFilter class does (indirectly via UriComponentBuilder).
 * Sadly some proxies (including Google IAP and Traefik) set X-Forwarded-Proto only, thus causing problems with CorsFilter and potentially other code.
 * This filter "patches" the incorrect request headers to add the missing header that the proxy _should_ have set.
 * </p>
 * <p>
 * See https://jira.spring.io/browse/SPR-16262 for more information on the spring-cors problem caused by the missing header.
 * </p>
 */
@Configuration
public class ForwardedHeaderConfig {
    /**
     * A servlet-engine has an ordered list of Filter instances to apply to a request. However spring has invented type FilterChainProxy which looks
     * to the servlet-engine like a single filter but internally is a list of filters to apply. Spring-security creates a single bean of name
     * springSecurityFilterChain (type FilterChainProxy) which contains all the security-module-relevant filter objects. That chain object
     * has an associated "order" value of -100 (where lower values are registered first in the "real" servlet-engine filter list). In order for
     * the AddForwardPortFilter filter to be executed _before_ the CorsFilter (which is part of the securityFilterChain), it must be registered
     * with an order less than -100.
     */
    private static final int ORDER_BEFORE_SECURITY = -200;

    /**
     * Register a filter that adds in an X-Forwarded-Port http-header if desired.
     * <p>
     * It is not possible to dynamically decide whether to return a bean here or not - the return value must always be non-null. However
     * it is possible to return a bean with property enabled=false (ie call setEnabled on the returned object), in which case the referenced
     * filter object will not be registered.
     * </p>
     */
    @Bean
    FilterRegistrationBean addForwardedPortHeader() {
        Filter filter = new AddForwardPortFilter();
        FilterRegistrationBean frb = new FilterRegistrationBean(filter);
        frb.setOrder(ORDER_BEFORE_SECURITY);
        // optionally call frb.setEnabled(false);
        return frb;
    }

    // =================================================================================================================================

    private static class AddForwardPortFilter implements Filter {
        private static final String HEADER_FORWARDED_PROTO = "X-Forwarded-Proto";
        private static final String HEADER_FORWARDED_PORT = "X-Forwarded-Port";

        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
        }

        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

            // If request has X-Forwarded-Proto of "https" but not "X-Forwarded-Port" then set the forwarded port to 443.
            // The proxy which did the forwarding really should have set the -Port header - but we can do it for them here.
            HttpServletRequest req = (HttpServletRequest) request;
            String forwardedProto = req.getHeader(HEADER_FORWARDED_PROTO);
            String forwardedPort = req.getHeader(HEADER_FORWARDED_PORT);
            if ("https".equals(forwardedProto) && (forwardedPort == null)) {
                request = new AddHeaderRequest(req, HEADER_FORWARDED_PORT, "443");
            }

            chain.doFilter(request, response);
        }

        @Override
        public void destroy() {
        }
    }

    /**
     * Trivial class to "set an http header" on an incoming request.
     * <p>
     * Type HttpServletRequest does not provide a "set header" method - headers are immutable. It is therefore necessary to wrap the
     * original request and pass all calls through unaltered except for "getHeader"...
     * </p>
     */
    private static class AddHeaderRequest extends HttpServletRequestWrapper {
        private final String headerName;
        private final String headerValue;

        AddHeaderRequest(HttpServletRequest req, String headerName, String headerValue) {
            super(req);
            this.headerName = headerName;
            this.headerValue = headerValue;
        }

        @Override
        public String getHeader(String name) {
            if (headerName.equals(name)) {
                return headerValue;
            }

            return super.getHeader(name);
        }

        @Override
        public Enumeration<String> getHeaders(String name) {
            if (headerName.equals(name)) {
                return Collections.enumeration(Collections.singletonList(headerValue));
            }

            return super.getHeaders(name);
        }

        @Override
        public Enumeration<String> getHeaderNames() {
            return new ChainedEnumeration<String>(
                    super.getHeaderNames(),
                    Collections.enumeration(Collections.singletonList(headerName)));
        }
    }

    /**
     * Trivial class to make a single enumeration return data from multiple collections.
     */
    private static class ChainedEnumeration<T> implements Enumeration<T>  {
        Enumeration<T> first;
        Enumeration<T> second;
        ChainedEnumeration(Enumeration<T> first, Enumeration<T> second) {
            this.first = first;
            this.second = second;
        }

        @Override
        public boolean hasMoreElements() {
            return first.hasMoreElements() || second.hasMoreElements();
        }

        @Override
        public T nextElement() {
            if (first.hasMoreElements()) {
                return first.nextElement();
            } else {
                return second.nextElement();
            }
        }
    }
}

How it Works

A Spring @Configuration class can be used to configure web security features via the HttpSecurity object. If method HttpSecurity.cors() is invoked, then an instance of class CorsFilter is added to the springSecurityFilterChain bean, which manages a list of http-filter objects. The CorsFilter instance checks each request for header “origin”, and if present then verifies that the origin matches the current request’s URL (ie the address at which the current webapp is available) or that it is one of the “allowed” origins.

CORS checks have two parts. Web-browsers are responsible for tracking the original location from which each html or javascript document has been loaded; when html or javascript wishes to submit a request to a remote server then:

  • if this is a “simple” request then it is just allowed, and the server is responsible for doing any checks
  • if this is a “complex” request then the browser applies a “same-origin” test: if the html/javascript initiating the request was loaded from the same host:port that the request is being sent to, allow. Otherwise send an OPTIONS http request to the target URL (called a “pre-flight request”), and use the returned info to decide whether to allow the request or block it in the browser itself.

For either simple or complex requests, the browser should add an “origin” http header to the subsequent request, indicating the URL from which the html or javascript that initiated the request came from.

Normally, applying security rules on the client side is a sign of design problems, but in this case the browser is protecting its user against bad code loaded from one site trying to perform operations on behalf of its user on another site. Client-side security checks are therefore appropriate here.

However the particular problem we are talking about here is in verifying the “real” requests on the server-side, not dealing with the OPTIONS “pre-flight” request. And the problem is that after passing through an external proxy which transforms an HTTPS request into an HTTP one, the “origin” header and the incoming URL really do not match - it looks like an invalid cross-origin request. The solution of course is to include the X-Forwarded-* headers when comparing incoming URL with origin header - but this only works when the proxy sets them.

The solution is simply to add a filter that runs before the spring CorsFilter, and which adds in an X-Forwarded-Port header if it is missing.

There are two minor complications:

  • getting a filter to run before the CorsFilter is tricky, and
  • the HttpServletRequest class has getHeader methods, but not setHeader methods.

Standard Java servlet engines have a concept of an ordered list of filters to apply. However Spring implements its own “filter chain” concept which to the servlet engine looks like a single filter, but is actually a list of filters. All of the security-related “filters” get added to this springSecurityFilterChain internal filter-chain object, which has an “ordering” value of -100. In order to get Spring to register our custom filter with the servlet-engine before the springSecurityFilterChain starts, it must therefore have an “order” property of less than -100.

Adding headers is done through a slightly ugly but reasonably standard approach: wrapping the http-request object and intercepting calls to getHeader and related methods.

Further Reading

More information on CORS: