CSRF跨域

Dav*_*vid 8 csrf spring-security cross-domain

我的 REST API 后端当前使用基于 cookie 的 CSRF 保护。

基本过程是后端设置一个可由客户端应用程序读取的 cookie,然后在后续的 HXR 请求(我的 CORS 设置允许)中,自定义标头与 cookie 一起传递,服务器检查两个值是否匹配。

本质上,这一切都是通过 Spring Security 中的一行非常简单的 Java 代码启用的。

.csrf().csrfTokenRepository(new CookieCsrfTokenRepository())
Run Code Online (Sandbox Code Playgroud)

当 UI 从同一域提供服务时,这非常有效,因为客户端中的 JS 可以轻松访问(非 http-only)cookie 来读取值并发送自定义标头。

当我希望将我的客户端应用程序部署在不同的域上时,我面临的挑战就来了,例如

API: api.x.com
UI: ui.y.com
Run Code Online (Sandbox Code Playgroud)

我解决这个问题的想法是

  1. 令牌可以与 cookie 一起在自定义响应标头中发回,而不是仅在 cookie 中发回。
  2. 然后,客户端读取自定义标头和本地存储(使用本地存储或者可能通过在客户端动态创建 cookie,但这次是在 UI 域上,以便稍后可以读取)。
  3. 随后,客户端在自定义请求标头中发出 XHR 请求时将使用该值,并且步骤 1 中设置的 cookie 也将随之使用。
  4. 服务器检查这两个值(cookie 和请求标头)是否已设置并且它们是否完全匹配。

这是一种众所周知/可接受的方法吗?任何人都可以从安全角度识别这种方法的任何明显缺陷吗?

显然,API 服务器需要允许 UI 域使用 CORS + 允许凭据并在 CORS 策略中公开自定义响应标头。

编辑

我将尝试使用我编写的自定义存储库在 Spring Security 中实现此目的:

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

Dav*_*vid 4

我已经在生产中成功使用与我的描述编辑中的类类似的类大约一年了。班级是:

/**
 * 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="/sf/ask/3179714751/">/sf/ask/3179714751/</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("");
    }
}
Run Code Online (Sandbox Code Playgroud)

我通过 spring boot 安全配置启用它:

@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);
    }

}
Run Code Online (Sandbox Code Playgroud)

WebSecurityConfig请注意,我还为本文中所示的类启用了 CORS 属性源 bean,以将相关 XSRF 标头列入白名单:

@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;
    }
Run Code Online (Sandbox Code Playgroud)