首页 文章

CSRF跨域

提问于
浏览
3

我的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

我解决这个问题的想法是

  • 而不是仅仅在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);
    }
}

2 回答

  • 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())
    
  • 0

    我已成功使用类似于我的描述编辑的类在 生产环境 中大约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://stackoverflow.com/questions/45424496/csrf-cross-domain">https://stackoverflow.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);
        }
    
    }
    

    请注意,我还确实将CORS属性源bean启用到此帖子中显示的 WebSecurityConfig 类,以将相关的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;
        }
    

相关问题