Search code examples
springspring-bootspring-securitycsrf

CSRF on spring cloud gateway removing formData from POST requests 400 bad request error


I have enabled CSRF on my spring cloud api gateway server. I have angular as my GUI framework which calls the rest services through the api gateway.

I have used a custom filter to add the CSRF token to the response headers.

When the POST call is made I see that the formData is lost. So I always get 400 Bad request errors.

I disabled CSRF and the request goes through fine without any issues.

Is there something wrong?

Below is my spring cloud gateway configuration. Gateway is used only for routing the requests to other microservices, it does not have any controllers or rest endpoints.

@SpringBootApplication
public class GatewayApplication {

@Autowired
ProfileManager profileManager;

@PostConstruct
public void onInit() {
    profileManager.printActiveProfiles();
}

public static void main(String[] args) { SpringApplication.run(GatewayApplication.class, args); }
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
    http.authorizeExchange().anyExchange().permitAll();
    http.csrf().csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse());
    return http.build();
   }
}

below is the filter code

@Component
public class CsrfHeaderFilter implements WebFilter {

@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
    Mono<CsrfToken> token = (Mono<CsrfToken>) exchange.getAttributes().get(CsrfToken.class.getName());
    if (token != null) {
        return token.flatMap(t -> chain.filter(exchange));
    }
    return chain.filter(exchange);
}

}

My POST rest endpoints are defined with

@RequestParam

below is the code from one of the rest service endpoints. It is an upstream service implemented using the traditional servlet springboot framework.

@RequestMapping(value = "terminate/{listName}", method = RequestMethod.POST, consumes = MediaType.APPLICATION_FORM_URLENCODED)
@CrossOrigin
@Loggable (activityname = ActivityLogConstants.DESCRIPTOR_TERMINATE)
public Response terminate(@Context HttpServletRequest reqContext, @PathVariable String listName, @RequestParam(value = "rowData") String rowData)
        throws ServiceException {....}

The formData is lost by the time the request reaches the upstream services.

Looks like the filter in spring cloud gateways is blocking formData

here is my netty configuration:

@Configuration
public class NettyConfiguration implements WebServerFactoryCustomizer<NettyReactiveWebServerFactory> {

@Value("${server.max-initial-line-length:65536}")
private int maxInitialLingLength;
@Value("${server.max-http-header-size:65536}")
private int maxHttpHeaderSize;

public void customize(NettyReactiveWebServerFactory container) {
    container.addServerCustomizers(
            httpServer -> httpServer.httpRequestDecoder(
                    httpRequestDecoderSpec -> {
                        httpRequestDecoderSpec.maxHeaderSize(maxHttpHeaderSize);
                        httpRequestDecoderSpec.maxInitialLineLength(maxInitialLingLength);
                        return httpRequestDecoderSpec;
                    }
            )
    );
}
}

below is my application.yml enter image description here

sample log:

2022-07-28 09:18:20.743 DEBUG 26532 --- [ctor-http-nio-5] r.n.http.client.HttpClientOperations : [id:199cd714-5, L:/127.0.0.1:50342 - R:localhost/127.0.0.1:18080] Received response (auto-read:false) : [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, X-Application-Context=application:18080, Date=Thu, 28 Jul 2022 03:48:20 GMT, Connection=close, content-length=0] 2022-07-28 09:18:20.744 DEBUG 26532 --- [ctor-http-nio-5] r.n.r.DefaultPooledConnectionProvider : [id:199cd714-5, L:/127.0.0.1:50342 - R:localhost/127.0.0.1:18080] onStateChange(POST{uri=/cms-service/webapi/terminate/descriptor, connection=PooledConnection{channel=[id: 0x199cd714, L:/127.0.0.1:50342 - R:localhost/127.0.0.1:18080]}}, [response_received]) 2022-07-28 09:18:20.744 DEBUG 26532 --- [ctor-http-nio-5] reactor.netty.channel.FluxReceive : [id:199cd714-5, L:/127.0.0.1:50342 - R:localhost/127.0.0.1:18080] FluxReceive{pending=0, cancelled=false, inboundDone=false, inboundError=null}: subscribing inbound receiver 2022-07-28 09:18:20.744 DEBUG 26532 --- [ctor-http-nio-5] r.n.http.client.HttpClientOperations : [id:199cd714-5, L:/127.0.0.1:50342 - R:localhost/127.0.0.1:18080] Received last HTTP packet 2022-07-28 09:18:20.744 DEBUG 26532 --- [ctor-http-nio-5] r.n.http.server.HttpServerOperations : [id:b0f975eb-11, L:/0:0:0:0:0:0:0:1:10443 - R:/0:0:0:0:0:0:0:1:50337] Decreasing pending responses, now 0 2022-07-28 09:18:20.745 DEBUG 26532 --- [ctor-http-nio-5] r.n.http.server.HttpServerOperations : [id:b0f975eb-11, L:/0:0:0:0:0:0:0:1:10443 - R:/0:0:0:0:0:0:0:1:50337] Last HTTP packet was sent, terminating the channel 2022-07-28 09:18:20.745 DEBUG 26532 --- [ctor-http-nio-5] o.s.w.s.adapter.HttpWebHandlerAdapter : [b0f975eb-11, L:/0:0:0:0:0:0:0:1:10443 - R:/0:0:0:0:0:0:0:1:50337] Completed 400 BAD_REQUEST 2022-07-28 09:18:20.745 DEBUG 26532 --- [ctor-http-nio-5] r.n.http.server.HttpServerOperations : [id:b0f975eb-11, L:/0:0:0:0:0:0:0:1:10443 - R:/0:0:0:0:0:0:0:1:50337] Last HTTP response frame 2022-07-28 09:18:20.745 DEBUG 26532 --- [ctor-http-nio-5] c.m.webgateway.handler.RequestLogger : Total time required to process /cms-service/webapi/terminate/descriptor request 60055 2022-07-28 09:18:20.745 DEBUG 26532 --- [ctor-http-nio-5] r.n.r.DefaultPooledConnectionProvider : [id:199cd714, L:/127.0.0.1:50342 - R:localhost/127.0.0.1:18080] onStateChange(POST{uri=/cms-service/webapi/terminate/descriptor, connection=PooledConnection{channel=[id: 0x199cd714, L:/127.0.0.1:50342 - R:localhost/127.0.0.1:18080]}}, [response_completed]) 2022-07-28 09:18:20.745 DEBUG 26532 --- [ctor-http-nio-5] r.n.r.DefaultPooledConnectionProvider : [id:199cd714, L:/127.0.0.1:50342 - R:localhost/127.0.0.1:18080] onStateChange(POST{uri=/cms-service/webapi/terminate/descriptor, connection=PooledConnection{channel=[id: 0x199cd714, L:/127.0.0.1:50342 - R:localhost/127.0.0.1:18080]}}, [disconnecting]) 2022-07-28 09:18:20.752 DEBUG 26532 --- [ctor-http-nio-5] r.n.resources.PooledConnectionProvider : [id:199cd714, L:/127.0.0.1:50342 ! R:localhost/127.0.0.1:18080] Channel closed, now: 0 active connections, 4 inactive connections and 0 pending acquire requests. 2022-07-28 09:18:20.752 DEBUG 26532 --- [ctor-http-nio-5] r.n.r.DefaultPooledConnectionProvider : [id:199cd714, L:/127.0.0.1:50342 ! R:localhost/127.0.0.1:18080] onStateChange(PooledConnection{channel=[id: 0x199cd714, L:/127.0.0.1:50342 ! R:localhost/127.0.0.1:18080]}, [disconnecting]) 2022-07-28 09:18:23.805 DEBUG 26532 --- [ctor-http-nio-5] r.n.http.server.HttpServerOperations : [id:b0f975eb, L:/0:0:0:0:0:0:0:1:10443 - R:/0:0:0:0:0:0:0:1:50337] Increasing pending responses, now 1 2022-07-28 09:18:23.805 DEBUG 26532 --- [ctor-http-nio-5] reactor.netty.http.server.HttpServer : [id:b0f975eb-12, L:/0:0:0:0:0:0:0:1:10443 - R:/0:0:0:0:0:0:0:1:50337] Handler is being applied: org.springframework.http.server.reactive.ReactorHttpHandlerAdapter@7c82616c 2022-07-28 09:18:23.805 DEBUG 26532 --- [ctor-http-nio-5] o.s.w.s.adapter.HttpWebHandlerAdapter : [b0f975eb-12, L:/0:0:0:0:0:0:0:1:10443 - R:/0:0:0:0:0:0:0:1:50337] HTTP GET "/cms-service/webapi/data/descriptor"

below is the link to the sample project. https://github.com/manjosh1990/webgateway-issues

I tried to ignore FORM URL ENCODED requests and GET request, but it still does not work

private static final Set<HttpMethod> ALLOWED_METHODS = new HashSet<>(
        Arrays.asList(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.TRACE, HttpMethod.OPTIONS));
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
    http.authorizeExchange().anyExchange().permitAll().and()
            .csrf(csrf -> csrf
                    .requireCsrfProtectionMatcher(ignoringFormUrlEncodedContentType())
                    .csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse()));
    return http.build();
}
private ServerWebExchangeMatcher ignoringFormUrlEncodedContentType() {
    return (exchange) -> !MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(
            exchange.getRequest().getHeaders().getContentType()) || !ALLOWED_METHODS.contains(exchange.getRequest().getMethod())
            ? ServerWebExchangeMatcher.MatchResult.match()
            : ServerWebExchangeMatcher.MatchResult.notMatch();
}

Solution

  • Thanks for the minimal sample to reproduce the issue!

    After some testing, I'm unable to come up with a workaround or fix for your configuration that allows a form post (URL-encoded) to pass through the gateway with CSRF protection enabled. My best guess is it has to do with how Spring Security is consuming the request body (which should be cached for subsequent filters to consume) vs how Spring Cloud Gateway is consuming the request body in order to proxy to the downstream service.

    I tested this by disabling CSRF protection and adding the following filter:

    @Component
    public class TestWebFilter implements WebFilter {
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
            return Mono.defer(() -> exchange.getFormData()
                    .doOnSuccess(System.out::println))
                    .then(chain.filter(exchange));
        }
    }
    

    In my testing, this causes the request through the gateway to block for a long time before receiving:

    {
        "timestamp": "2022-08-10T19:13:54.265+00:00",
        "status": 400,
        "error": "Bad Request",
        "path": "/cms-service/webapi/service/post/test"
    }
    

    Since this appears to be a bug in Spring Security, I'd recommend submitting a bug in Spring Security and we can work through it from there.

    If you would like to work around the issue in the meantime, you can disable CSRF protection for these types of requests, as follows:

    @Configuration
    @EnableWebFluxSecurity
    public class SecurityConfig {
    
        @Bean
        public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
            http
                .authorizeExchange((authorize) -> authorize
                    .anyExchange().authenticated()
                )
                .csrf((csrf) -> csrf
                    .requireCsrfProtectionMatcher(ignoringFormUrlEncodedContentType())
                    .csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse())
                )
                .oauth2ResourceServer(ServerHttpSecurity.OAuth2ResourceServerSpec::jwt);
            return http.build();
        }
    
        private ServerWebExchangeMatcher ignoringFormUrlEncodedContentType() {
            return (exchange) -> !MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(
                exchange.getRequest().getHeaders().getContentType())
                    ? ServerWebExchangeMatcher.MatchResult.match()
                    : ServerWebExchangeMatcher.MatchResult.notMatch();
        }
    
    }
    

    Important: This is not ideal, because these requests won't be protected. However, this might make sense if these requests were never performed in a browser. In that case, it would make sense to have a separate authentication mechanism, such as requiring a bearer token instead of form login, etc. (as in the example above).