Improve CORS handling

This commit improves CORS support by:
 - Using CORS processing only for CORS-enabled endpoints
 - Skipping CORS processing for same-origin requests
 - Adding Vary headers for non-CORS requests

It introduces an AbstractHandlerMapping#hasCorsConfigurationSource
method in order to be able to check CORS endpoints efficiently.

Closes gh-22273
Closes gh-22496
This commit is contained in:
Sebastien Deleuze
2019-04-01 14:36:38 +02:00
parent 8714710170
commit d27b5d0ab6
20 changed files with 278 additions and 123 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2015 the original author or authors.
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,6 +20,10 @@ import javax.servlet.http.HttpServletRequest;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.lang.Nullable;
import org.springframework.util.ObjectUtils;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
/**
* Utility class for CORS request handling based on the
@@ -31,17 +35,43 @@ import org.springframework.http.HttpMethod;
public abstract class CorsUtils {
/**
* Returns {@code true} if the request is a valid CORS one.
* Returns {@code true} if the request is a valid CORS one by checking {@code Origin}
* header presence and ensuring that origins are different.
*/
public static boolean isCorsRequest(HttpServletRequest request) {
return (request.getHeader(HttpHeaders.ORIGIN) != null);
String origin = request.getHeader(HttpHeaders.ORIGIN);
if (origin == null) {
return false;
}
UriComponents originUrl = UriComponentsBuilder.fromOriginHeader(origin).build();
String scheme = request.getScheme();
String host = request.getServerName();
int port = request.getServerPort();
return !(ObjectUtils.nullSafeEquals(scheme, originUrl.getScheme()) &&
ObjectUtils.nullSafeEquals(host, originUrl.getHost()) &&
getPort(scheme, port) == getPort(originUrl.getScheme(), originUrl.getPort()));
}
private static int getPort(@Nullable String scheme, int port) {
if (port == -1) {
if ("http".equals(scheme) || "ws".equals(scheme)) {
port = 80;
}
else if ("https".equals(scheme) || "wss".equals(scheme)) {
port = 443;
}
}
return port;
}
/**
* Returns {@code true} if the request is a valid CORS pre-flight one.
* To be used in combination with {@link #isCorsRequest(HttpServletRequest)} since
* regular CORS checks are not invoked here for performance reasons.
*/
public static boolean isPreFlightRequest(HttpServletRequest request) {
return (isCorsRequest(request) && HttpMethod.OPTIONS.matches(request.getMethod()) &&
return (HttpMethod.OPTIONS.matches(request.getMethod()) &&
request.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD) != null);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,7 +19,6 @@ package org.springframework.web.cors;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -36,7 +35,6 @@ import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.lang.Nullable;
import org.springframework.util.CollectionUtils;
import org.springframework.web.util.WebUtils;
/**
* The default implementation of {@link CorsProcessor}, as defined by the
@@ -45,8 +43,7 @@ import org.springframework.web.util.WebUtils;
* <p>Note that when input {@link CorsConfiguration} is {@code null}, this
* implementation does not reject simple or actual requests outright but simply
* avoid adding CORS headers to the response. CORS processing is also skipped
* if the response already contains CORS headers, or if the request is detected
* as a same-origin one.
* if the response already contains CORS headers.
*
* @author Sebastien Deleuze
* @author Rossen Stoyanchev
@@ -62,26 +59,23 @@ public class DefaultCorsProcessor implements CorsProcessor {
public boolean processRequest(@Nullable CorsConfiguration config, HttpServletRequest request,
HttpServletResponse response) throws IOException {
response.addHeader(HttpHeaders.VARY, HttpHeaders.ORIGIN);
response.addHeader(HttpHeaders.VARY, HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD);
response.addHeader(HttpHeaders.VARY, HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS);
if (!CorsUtils.isCorsRequest(request)) {
return true;
}
ServletServerHttpResponse serverResponse = new ServletServerHttpResponse(response);
if (responseHasCors(serverResponse)) {
if (response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN) != null) {
logger.trace("Skip: response already contains \"Access-Control-Allow-Origin\"");
return true;
}
ServletServerHttpRequest serverRequest = new ServletServerHttpRequest(request);
if (WebUtils.isSameOrigin(serverRequest)) {
logger.trace("Skip: request is from same origin");
return true;
}
boolean preFlightRequest = CorsUtils.isPreFlightRequest(request);
if (config == null) {
if (preFlightRequest) {
rejectRequest(serverResponse);
rejectRequest(new ServletServerHttpResponse(response));
return false;
}
else {
@@ -89,17 +83,7 @@ public class DefaultCorsProcessor implements CorsProcessor {
}
}
return handleInternal(serverRequest, serverResponse, config, preFlightRequest);
}
private boolean responseHasCors(ServerHttpResponse response) {
try {
return (response.getHeaders().getAccessControlAllowOrigin() != null);
}
catch (NullPointerException npe) {
// SPR-11919 and https://issues.jboss.org/browse/WFLY-3474
return false;
}
return handleInternal(new ServletServerHttpRequest(request), new ServletServerHttpResponse(response), config, preFlightRequest);
}
/**
@@ -110,6 +94,7 @@ public class DefaultCorsProcessor implements CorsProcessor {
protected void rejectRequest(ServerHttpResponse response) throws IOException {
response.setStatusCode(HttpStatus.FORBIDDEN);
response.getBody().write("Invalid CORS request".getBytes(StandardCharsets.UTF_8));
response.flush();
}
/**
@@ -122,9 +107,6 @@ public class DefaultCorsProcessor implements CorsProcessor {
String allowOrigin = checkOrigin(config, requestOrigin);
HttpHeaders responseHeaders = response.getHeaders();
responseHeaders.addAll(HttpHeaders.VARY, Arrays.asList(HttpHeaders.ORIGIN,
HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS));
if (allowOrigin == null) {
logger.debug("Reject: '" + requestOrigin + "' origin is not allowed");
rejectRequest(response);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -36,18 +36,21 @@ import org.springframework.web.util.UriComponentsBuilder;
public abstract class CorsUtils {
/**
* Returns {@code true} if the request is a valid CORS one.
* Returns {@code true} if the request is a valid CORS one by checking {@code Origin}
* header presence and ensuring that origins are different via {@link #isSameOrigin}.
*/
@SuppressWarnings("deprecation")
public static boolean isCorsRequest(ServerHttpRequest request) {
return (request.getHeaders().get(HttpHeaders.ORIGIN) != null);
return request.getHeaders().containsKey(HttpHeaders.ORIGIN) && !isSameOrigin(request);
}
/**
* Returns {@code true} if the request is a valid CORS pre-flight one.
* To be used in combination with {@link #isCorsRequest(ServerHttpRequest)} since
* regular CORS checks are not invoked here for performance reasons.
*/
public static boolean isPreFlightRequest(ServerHttpRequest request) {
return (request.getMethod() == HttpMethod.OPTIONS && isCorsRequest(request) &&
request.getHeaders().get(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD) != null);
return (request.getMethod() == HttpMethod.OPTIONS && request.getHeaders().containsKey(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD));
}
/**
@@ -61,7 +64,9 @@ public abstract class CorsUtils {
*
* @return {@code true} if the request is a same-origin one, {@code false} in case
* of a cross-origin request
* @deprecated as of 5.2, same-origin checks are performed directly by {@link #isCorsRequest}
*/
@Deprecated
public static boolean isSameOrigin(ServerHttpRequest request) {
String origin = request.getHeaders().getOrigin();
if (origin == null) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -75,14 +75,10 @@ public class CorsWebFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
if (CorsUtils.isCorsRequest(request)) {
CorsConfiguration corsConfiguration = this.configSource.getCorsConfiguration(exchange);
if (corsConfiguration != null) {
boolean isValid = this.processor.process(corsConfiguration, exchange);
if (!isValid || CorsUtils.isPreFlightRequest(request)) {
return Mono.empty();
}
}
CorsConfiguration corsConfiguration = this.configSource.getCorsConfiguration(exchange);
boolean isValid = this.processor.process(corsConfiguration, exchange);
if (!isValid || CorsUtils.isPreFlightRequest(request)) {
return Mono.empty();
}
return chain.filter(exchange);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -40,8 +40,7 @@ import org.springframework.web.server.ServerWebExchange;
* <p>Note that when input {@link CorsConfiguration} is {@code null}, this
* implementation does not reject simple or actual requests outright but simply
* avoid adding CORS headers to the response. CORS processing is also skipped
* if the response already contains CORS headers, or if the request is detected
* as a same-origin one.
* if the response already contains CORS headers.
*
* @author Sebastien Deleuze
* @author Rossen Stoyanchev
@@ -51,27 +50,26 @@ public class DefaultCorsProcessor implements CorsProcessor {
private static final Log logger = LogFactory.getLog(DefaultCorsProcessor.class);
private static final List<String> VARY_HEADERS = Arrays.asList(
HttpHeaders.ORIGIN, HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS);
@Override
public boolean process(@Nullable CorsConfiguration config, ServerWebExchange exchange) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
response.getHeaders().addAll(HttpHeaders.VARY, VARY_HEADERS);
if (!CorsUtils.isCorsRequest(request)) {
return true;
}
if (responseHasCors(response)) {
if (response.getHeaders().getFirst(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN) != null) {
logger.trace("Skip: response already contains \"Access-Control-Allow-Origin\"");
return true;
}
if (CorsUtils.isSameOrigin(request)) {
logger.trace("Skip: request is from same origin");
return true;
}
boolean preFlightRequest = CorsUtils.isPreFlightRequest(request);
if (config == null) {
if (preFlightRequest) {
@@ -86,10 +84,6 @@ public class DefaultCorsProcessor implements CorsProcessor {
return handleInternal(exchange, config, preFlightRequest);
}
private boolean responseHasCors(ServerHttpResponse response) {
return response.getHeaders().getFirst(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN) != null;
}
/**
* Invoked when one of the CORS checks failed.
*/
@@ -107,9 +101,6 @@ public class DefaultCorsProcessor implements CorsProcessor {
ServerHttpResponse response = exchange.getResponse();
HttpHeaders responseHeaders = response.getHeaders();
response.getHeaders().addAll(HttpHeaders.VARY, Arrays.asList(HttpHeaders.ORIGIN,
HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS));
String requestOrigin = request.getHeaders().getOrigin();
String allowOrigin = checkOrigin(config, requestOrigin);
if (allowOrigin == null) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -83,16 +83,11 @@ public class CorsFilter extends OncePerRequestFilter {
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
if (CorsUtils.isCorsRequest(request)) {
CorsConfiguration corsConfiguration = this.configSource.getCorsConfiguration(request);
if (corsConfiguration != null) {
boolean isValid = this.processor.processRequest(corsConfiguration, request, response);
if (!isValid || CorsUtils.isPreFlightRequest(request)) {
return;
}
}
CorsConfiguration corsConfiguration = this.configSource.getCorsConfiguration(request);
boolean isValid = this.processor.processRequest(corsConfiguration, request, response);
if (!isValid || CorsUtils.isPreFlightRequest(request)) {
return;
}
filterChain.doFilter(request, response);
}