CSRF跨域

时间:2017-07-31 20:10:49

标签: spring-security cors csrf csrf-protection

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

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

基本上,所有这些都是在Spring安全性中使用一个非常简单的Java代码行启用的。

.csrf().csrfTokenRepository(new CookieCsrfTokenRepository())

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

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

API: api.x.com
UI: ui.y.com

我解决这个问题的想法是

  1. 不是仅仅在一个cookie中发送令牌,而是可以在自定义响应头中发送回来,也可以与cookie一起发回。
  2. 客户端然后读取自定义标头和本地存储(使用本地存储或者可能通过在客户端动态创建cookie,但这次是在UI域上,以便以后可以读取它。)
  3. 当客户端在自定义请求标头中发出XHR请求时,客户端随后会使用此值,并且步骤1中设置的cookie也将随之一起使用。
  4. 服务器检查这两个值(cookie和请求标头)是否已设置且它们是否完全匹配。
  5. 这是众所周知/可接受的方法吗?从安全角度来看,任何人都可以通过这种方法找出任​​何明显的缺陷。

    显然,API服务器需要允许CORS用于UI域+允许凭证并在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);
        }
    }
    

1 个答案:

答案 0 :(得分:0)

我认为您可以为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())