CSRF 跨域

Posted

技术标签:

【中文标题】CSRF 跨域【英文标题】:CSRF Cross Domain 【发布时间】:2018-01-07 12:48:57 【问题描述】:

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

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

基本上,这一切都是通过 Spring Security 中的一行非常简单的 Java 代码实现的。

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

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

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

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

我解决这个问题的想法是

    可以将令牌与 cookie 一起发送回自定义响应标头中,而不是仅在 cookie 中发回。 然后客户端读取自定义标头和本地消息(使用本地存储或可能通过在客户端动态创建 cookie,但这次是在 UI 域上,以便稍后读取)。 随后,当客户端在自定义请求标头中发出 XHR 请求时,该值将被客户端使用,并且在步骤 1 中设置的 cookie 也将随之使用。 服务器检查这两个值(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);
    

【问题讨论】:

我在这行 response.setHeader(token.getHeaderName(), token.getToken()); 中得到了 NPE,但我只是添加了一个空验证,它就可以工作了。 有一个 PR 请求来处理这个github.com/spring-projects/spring-security/issues/4315 @EranMedan 我查看了您在上面发布的 PR。我似乎无法弄清楚我需要更改哪些依赖项才能获得此修复。我现在正在使用 spring-boot-starter-security 。你知道我需要在依赖项列表中更改什么吗? @alphathesis 我认为它还没有在 Maven 中心,例如唯一的方法是从源代码克隆和构建,然后 mvn 安装,然后作为依赖项包含,但同时复制修改后的类可能更容易 附注我会小心使用自定义标头 + 本地保存的想法,这听起来像是一个可能的解决方案,但这并未列在 OWASP CSRF 缓解列表中(这并不意味着它一定有缺陷)但我会对其进行测试并与一些渗透测试员/应用程序安全专家一起审查它,然后再宣布它是安全的。我至少会添加 Origin 标头过滤器(只需忽略任何没有 Origin 或 Referer 的 POST 请求,并为您应用的所有 XHR 请求添加自定义标头,确保您有良好的 CORS 标头、CSP、HSTS 等。 【参考方案1】:

大约 1 年以来,我一直在成功地使用类似于我在生产中的描述编辑中的类。班级是:

/**
 * 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://***.com/questions/45424496/csrf-cross-domain">https://***.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 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);
    


请注意,我还为本文中显示的 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;
    

【讨论】:

【参考方案2】:

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

【讨论】:

感谢您的评论,基本上这就是我认为我最终所做的。我发布了我最终使用的代码。感谢您的样品。

以上是关于CSRF 跨域的主要内容,如果未能解决你的问题,请参考以下文章

html5 postMessage解决跨域跨窗口消息传递

html5 postMessage解决跨域跨窗口消息传递

iframe可通过postMessage解决跨域跨窗口消息传递

html5 postMessage解决跨域跨窗口消息传递

session生命与跨域跨页面---小疑惑大难题(php)

跨域跨服务器调用时候session丢失的问题