CSRF跨域
CSRF Cross Domain
我的 REST API 后端当前使用基于 cookie 的 CSRF 保护。
基本过程是后端设置一个 cookie,客户端应用程序可以读取该 cookie,然后在后续的 HXR 请求(我的 CORS 设置允许)中传递自定义 header cookie 和服务器检查这两个值是否匹配。
本质上,这一切都是通过 spring 安全性中的一行非常简单的 Java 代码实现的。
.csrf().csrfTokenRepository(new CookieCsrfTokenRepository())
当 UI 从同一个域提供服务时,这非常有效,因为客户端中的 JS 可以轻松访问 (non-http-only) cookie 以读取值并发送自定义 header.
当我希望我的客户端应用程序部署在不同的域上时,我的挑战就来了,例如
API: api.x.com
UI: ui.y.com
我的解决办法是
- 与其仅在 cookie 中发回令牌,还可以在自定义响应中发回令牌 header,也可与 cookie 一起发回。
- 客户端然后读取自定义 header 和本地 sores(使用本地存储或者可能通过在客户端动态创建 cookie,但这次是在 UI 域上以便它以后可以看)。
- 客户端随后在自定义请求中发出 XHR 请求时使用此值 header,并且在步骤 1 中设置的 cookie 也将随之使用。
- 服务器检查这两个值(cookie 和请求 header)是否已设置并且它们是否完全匹配。
这是一个很好的 known/acceptable 方法吗?任何人都可以从安全角度识别这种方法的任何明显缺陷。
显然,API 服务器需要允许 UI 域的 CORS + 允许凭据并在 CORS 策略中公开自定义响应 header。
编辑
我将尝试使用我编写的自定义存储库在 Spring 安全性中实现此目的:
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.CsrfTokenRepository;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* This class is essentially a wrapper for a cookie based CSRF protection scheme.
* <p>
* The issue with the pure cookie based mechanism is that if you deploy the UI on a different domain to the API then the client is not able to read the cookie value when a new CSRF token is generated (even if the cookie is not HTTP only).
* <p>
* This mechanism essentially does the same thing, but also provides a response header so that the client can read this value and the use some local mechanism to store the token (session storage, local storage, local user agent DB, construct a new cookie on the UI domain etc).
*/
public class CrossDomainHeaderAndCookieCsrfTokenRepository implements CsrfTokenRepository {
public static final String XSRF_HEADER_NAME = "X-XSRF-TOKEN";
private static final String XSRF_TOKEN_COOKIE_NAME = "XSRF-TOKEN";
private static final String CSRF_QUERY_PARAM_NAME = "_csrf";
private final CookieCsrfTokenRepository delegate = new CookieCsrfTokenRepository();
public CrossDomainHeaderAndCookieCsrfTokenRepository() {
delegate.setCookieHttpOnly(true);
delegate.setHeaderName(XSRF_HEADER_NAME);
delegate.setCookieName(XSRF_TOKEN_COOKIE_NAME);
delegate.setParameterName(CSRF_QUERY_PARAM_NAME);
}
@Override
public CsrfToken generateToken(final HttpServletRequest request) {
return delegate.generateToken(request);
}
@Override
public void saveToken(final CsrfToken token, final HttpServletRequest request, final HttpServletResponse response) {
delegate.saveToken(token, request, response);
response.setHeader(token.getHeaderName(), token.getToken());
}
@Override
public CsrfToken loadToken(final HttpServletRequest request) {
return delegate.loadToken(request);
}
}
我认为您可以为 CsrfTokenRepository 提供另一种实现,以支持 CSRF 令牌的不同域模式。
您可以通过对代码进行以下更改来克隆原始实现:
....
private String domain;
private Pattern domainPattern;
....
public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) {
....
String domain = getDomain(request);
if (domain != null) {
cookie.setDomain(domain);
}
response.addCookie(cookie);
}
.....
public void setDomainPattern(String domainPattern) {
if (this.domain != null) {
throw new IllegalStateException("Cannot set both domainName and domainNamePattern");
}
this.domainPattern = Pattern.compile(domainPattern, Pattern.CASE_INSENSITIVE);
}
public void setDomain(String domain) {
if (this.domainPattern != null) {
throw new IllegalStateException("Cannot set both domainName and domainNamePattern");
}
this.domain = domain;
}
private String getDomain(HttpServletRequest request) {
if (this.domain != null) {
return this.domain;
}
if (this.domainPattern != null) {
Matcher matcher = this.domainPattern.matcher(request.getServerName());
if (matcher.matches()) {
return matcher.group(1);
}
}
return null;
}
然后,提供您的新实现。
.csrf().csrfTokenRepository(new CustomCookieCsrfTokenRepository())
我已经成功地使用 class 类似于我在生产中编辑描述的那个,现在已经大约 1 年了。 class 是:
/**
* This class is essentially a wrapper for a cookie based CSRF protection scheme.
* The issue with the pure cookie based mechanism is that if you deploy the UI on a different domain to the API then
* the client is not able to read the cookie value when a new CSRF token is generated (even if the cookie is not HTTP only).
* This mechanism does the same thing, but also provides a response header so that the client can read this value and the use
* some local mechanism to store the token (local storage, local user agent DB, construct a new cookie on the UI domain etc).
*
* @see <a href="https://whosebug.com/questions/45424496/csrf-cross-domain">https://whosebug.com/questions/45424496/csrf-cross-domain</a>
*/
public class CrossDomainCsrfTokenRepository implements CsrfTokenRepository {
public static final String XSRF_HEADER_NAME = "X-XSRF-TOKEN";
public static final String XSRF_TOKEN_COOKIE_NAME = "XSRF-TOKEN";
private static final String CSRF_QUERY_PARAM_NAME = "_csrf";
private final CookieCsrfTokenRepository delegate = new CookieCsrfTokenRepository();
public CrossDomainCsrfTokenRepository() {
delegate.setCookieHttpOnly(true);
delegate.setHeaderName(XSRF_HEADER_NAME);
delegate.setCookieName(XSRF_TOKEN_COOKIE_NAME);
delegate.setParameterName(CSRF_QUERY_PARAM_NAME);
}
@Override
public CsrfToken generateToken(final HttpServletRequest request) {
return delegate.generateToken(request);
}
@Override
public void saveToken(final CsrfToken token, final HttpServletRequest request, final HttpServletResponse response) {
delegate.saveToken(token, request, response);
response.setHeader(XSRF_HEADER_NAME, nullSafeTokenValue(token));
}
@Override
public CsrfToken loadToken(final HttpServletRequest request) {
return delegate.loadToken(request);
}
private String nullSafeTokenValue(final CsrfToken token) {
return ofNullable(token)
.map(CsrfToken::getToken)
.orElse("");
}
}
我通过 spring 引导安全配置启用它:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CsrfTokenRepository csrfTokenRepository;
@Override
@SuppressWarnings("PMD.SignatureDeclareThrowsException")
protected void configure(final HttpSecurity http) throws Exception {
http.csrf().ignoringAntMatchers(CTM_RESOURCE).csrfTokenRepository(csrfTokenRepository);
}
}
请注意,我还为 WebSecurityConfig
class 中显示的 post 启用了 CORS 属性 源 bean,以将相关的 XSRF 列入白名单 headers:
@Bean
public UrlBasedCorsConfigurationSource corsConfigurationSource() {
final CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(properties.getAllowedOrigins());
configuration.setAllowedMethods(allHttpMethods());
configuration.setAllowedHeaders(asList(CrossDomainCsrfTokenRepository.XSRF_HEADER_NAME, CONTENT_TYPE));
configuration.setExposedHeaders(asList(LOCATION, CrossDomainCsrfTokenRepository.XSRF_HEADER_NAME));
configuration.setAllowCredentials(true);
configuration.setMaxAge(HOURS.toSeconds(properties.getMaxAgeInHours()));
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
我的 REST API 后端当前使用基于 cookie 的 CSRF 保护。
基本过程是后端设置一个 cookie,客户端应用程序可以读取该 cookie,然后在后续的 HXR 请求(我的 CORS 设置允许)中传递自定义 header cookie 和服务器检查这两个值是否匹配。
本质上,这一切都是通过 spring 安全性中的一行非常简单的 Java 代码实现的。
.csrf().csrfTokenRepository(new CookieCsrfTokenRepository())
当 UI 从同一个域提供服务时,这非常有效,因为客户端中的 JS 可以轻松访问 (non-http-only) cookie 以读取值并发送自定义 header.
当我希望我的客户端应用程序部署在不同的域上时,我的挑战就来了,例如
API: api.x.com
UI: ui.y.com
我的解决办法是
- 与其仅在 cookie 中发回令牌,还可以在自定义响应中发回令牌 header,也可与 cookie 一起发回。
- 客户端然后读取自定义 header 和本地 sores(使用本地存储或者可能通过在客户端动态创建 cookie,但这次是在 UI 域上以便它以后可以看)。
- 客户端随后在自定义请求中发出 XHR 请求时使用此值 header,并且在步骤 1 中设置的 cookie 也将随之使用。
- 服务器检查这两个值(cookie 和请求 header)是否已设置并且它们是否完全匹配。
这是一个很好的 known/acceptable 方法吗?任何人都可以从安全角度识别这种方法的任何明显缺陷。
显然,API 服务器需要允许 UI 域的 CORS + 允许凭据并在 CORS 策略中公开自定义响应 header。
编辑
我将尝试使用我编写的自定义存储库在 Spring 安全性中实现此目的:
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.CsrfTokenRepository;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* This class is essentially a wrapper for a cookie based CSRF protection scheme.
* <p>
* The issue with the pure cookie based mechanism is that if you deploy the UI on a different domain to the API then the client is not able to read the cookie value when a new CSRF token is generated (even if the cookie is not HTTP only).
* <p>
* This mechanism essentially does the same thing, but also provides a response header so that the client can read this value and the use some local mechanism to store the token (session storage, local storage, local user agent DB, construct a new cookie on the UI domain etc).
*/
public class CrossDomainHeaderAndCookieCsrfTokenRepository implements CsrfTokenRepository {
public static final String XSRF_HEADER_NAME = "X-XSRF-TOKEN";
private static final String XSRF_TOKEN_COOKIE_NAME = "XSRF-TOKEN";
private static final String CSRF_QUERY_PARAM_NAME = "_csrf";
private final CookieCsrfTokenRepository delegate = new CookieCsrfTokenRepository();
public CrossDomainHeaderAndCookieCsrfTokenRepository() {
delegate.setCookieHttpOnly(true);
delegate.setHeaderName(XSRF_HEADER_NAME);
delegate.setCookieName(XSRF_TOKEN_COOKIE_NAME);
delegate.setParameterName(CSRF_QUERY_PARAM_NAME);
}
@Override
public CsrfToken generateToken(final HttpServletRequest request) {
return delegate.generateToken(request);
}
@Override
public void saveToken(final CsrfToken token, final HttpServletRequest request, final HttpServletResponse response) {
delegate.saveToken(token, request, response);
response.setHeader(token.getHeaderName(), token.getToken());
}
@Override
public CsrfToken loadToken(final HttpServletRequest request) {
return delegate.loadToken(request);
}
}
我认为您可以为 CsrfTokenRepository 提供另一种实现,以支持 CSRF 令牌的不同域模式。
您可以通过对代码进行以下更改来克隆原始实现:
....
private String domain;
private Pattern domainPattern;
....
public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) {
....
String domain = getDomain(request);
if (domain != null) {
cookie.setDomain(domain);
}
response.addCookie(cookie);
}
.....
public void setDomainPattern(String domainPattern) {
if (this.domain != null) {
throw new IllegalStateException("Cannot set both domainName and domainNamePattern");
}
this.domainPattern = Pattern.compile(domainPattern, Pattern.CASE_INSENSITIVE);
}
public void setDomain(String domain) {
if (this.domainPattern != null) {
throw new IllegalStateException("Cannot set both domainName and domainNamePattern");
}
this.domain = domain;
}
private String getDomain(HttpServletRequest request) {
if (this.domain != null) {
return this.domain;
}
if (this.domainPattern != null) {
Matcher matcher = this.domainPattern.matcher(request.getServerName());
if (matcher.matches()) {
return matcher.group(1);
}
}
return null;
}
然后,提供您的新实现。
.csrf().csrfTokenRepository(new CustomCookieCsrfTokenRepository())
我已经成功地使用 class 类似于我在生产中编辑描述的那个,现在已经大约 1 年了。 class 是:
/**
* This class is essentially a wrapper for a cookie based CSRF protection scheme.
* The issue with the pure cookie based mechanism is that if you deploy the UI on a different domain to the API then
* the client is not able to read the cookie value when a new CSRF token is generated (even if the cookie is not HTTP only).
* This mechanism does the same thing, but also provides a response header so that the client can read this value and the use
* some local mechanism to store the token (local storage, local user agent DB, construct a new cookie on the UI domain etc).
*
* @see <a href="https://whosebug.com/questions/45424496/csrf-cross-domain">https://whosebug.com/questions/45424496/csrf-cross-domain</a>
*/
public class CrossDomainCsrfTokenRepository implements CsrfTokenRepository {
public static final String XSRF_HEADER_NAME = "X-XSRF-TOKEN";
public static final String XSRF_TOKEN_COOKIE_NAME = "XSRF-TOKEN";
private static final String CSRF_QUERY_PARAM_NAME = "_csrf";
private final CookieCsrfTokenRepository delegate = new CookieCsrfTokenRepository();
public CrossDomainCsrfTokenRepository() {
delegate.setCookieHttpOnly(true);
delegate.setHeaderName(XSRF_HEADER_NAME);
delegate.setCookieName(XSRF_TOKEN_COOKIE_NAME);
delegate.setParameterName(CSRF_QUERY_PARAM_NAME);
}
@Override
public CsrfToken generateToken(final HttpServletRequest request) {
return delegate.generateToken(request);
}
@Override
public void saveToken(final CsrfToken token, final HttpServletRequest request, final HttpServletResponse response) {
delegate.saveToken(token, request, response);
response.setHeader(XSRF_HEADER_NAME, nullSafeTokenValue(token));
}
@Override
public CsrfToken loadToken(final HttpServletRequest request) {
return delegate.loadToken(request);
}
private String nullSafeTokenValue(final CsrfToken token) {
return ofNullable(token)
.map(CsrfToken::getToken)
.orElse("");
}
}
我通过 spring 引导安全配置启用它:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CsrfTokenRepository csrfTokenRepository;
@Override
@SuppressWarnings("PMD.SignatureDeclareThrowsException")
protected void configure(final HttpSecurity http) throws Exception {
http.csrf().ignoringAntMatchers(CTM_RESOURCE).csrfTokenRepository(csrfTokenRepository);
}
}
请注意,我还为 WebSecurityConfig
class 中显示的 post 启用了 CORS 属性 源 bean,以将相关的 XSRF 列入白名单 headers:
@Bean
public UrlBasedCorsConfigurationSource corsConfigurationSource() {
final CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(properties.getAllowedOrigins());
configuration.setAllowedMethods(allHttpMethods());
configuration.setAllowedHeaders(asList(CrossDomainCsrfTokenRepository.XSRF_HEADER_NAME, CONTENT_TYPE));
configuration.setExposedHeaders(asList(LOCATION, CrossDomainCsrfTokenRepository.XSRF_HEADER_NAME));
configuration.setAllowCredentials(true);
configuration.setMaxAge(HOURS.toSeconds(properties.getMaxAgeInHours()));
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}