写点什么

CSRF

作者:周杰伦本人
  • 2022 年 7 月 03 日
  • 本文字数:4147 字

    阅读完需:约 14 分钟

CSRF

CSRF(Cross-Site Request Forgery)跨站请求伪造,是一种挟持用户在当前已登录的浏览器上发送恶意请求的攻击方法,比如攻击者通过一些技术手段欺骗用户的浏览器,去访问一个用户曾经认证过的网站并执行恶意请求。由于客户端已经在该网站认证过,所以该网站会认为是用户在操作二执行恶意请求。


CSRF 的根源在于浏览器默认的身份验证机制(自动携带当前网站的 Cookie 信息),这种机制虽然可以保证请求是来自用户的某个浏览器,但是无法确保该请求是用户授权发送的,攻击者和用户发送的请求一模一样,如果在合法请求中额外携带一个攻击者无法获取的参数就可以成功区分出两种不同的请求,进而拒绝恶意请求


Spring 提供了两种机制:


  • 令牌同步模式

  • 在 Cookie 上知道 SameSite 属性

令牌同步模式

在每一个 HTTP 请求中,除了默认的自动携带的 Cookie 参数之外,再额外提供一个安全的,随机生成的字符串,我们称之为 CSRF 令牌。这个令牌由服务端生成,生成后在 HttpSession 中保存一份。当前请求到达之后,将请求携带的 CSRF 令牌信息和服务端中的令牌对比,如果两者不相等,则拒绝该 Http 请求。


<!DOCTYPE html><html lang="en" xmlns:th="http://www.thymeleaf.org"><head>    <meta charset="UTF-8">    <title>Title</title></head><body><form action="/hello" method="post">    <input type="hidden" th:value="${_csrf.token}" th:name="${_csrf.parameterName}">    <input type="submit" value="hello"></form></body></html>
复制代码


form 表单隐藏域中对应的 key 和 value 都是服务端默认返回的变量,只需要填充变量名即可。


请求方法要幂等性,所以 Spring Security 默认不会对 GET,HEAD,OPTIONS 以及 TRACE 请求进行 CSRF 令牌校验。


.csrf().disable();为关闭 CSRF 攻击防御功能,默认开启。


对应 form 表单请求,服务端返回的 CSRF 令牌,放在 request 属性中返回给前端。


对于 Ajax 请求,将 CSRF 令牌放在响应头 Cookie 中,开发者从 Cookie 中提取 CSRF 令牌信息,然后作为参数提交到服务端。


@Overrideprotected void configure(HttpSecurity http) throws Exception {    http.authorizeRequests()            .anyRequest().authenticated()            .and()            .formLogin()            .loginProcessingUrl("/login.html")            .successHandler((req,resp,auth)->{                resp.getWriter().write("login success");            })            .permitAll()            .and()            .headers()            .and()            .csrf()            .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());}
复制代码


csrfTokenRepository 为 CookieCsrfTokenRepository 设置 HttpOnly 为 false,否则前端无法获取到 Cookie 中 CSRF 令牌


<!DOCTYPE html><html lang="en"><head>    <meta charset="UTF-8">    <title>Title</title>    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-cookie/1.4.1/jquery.cookie.min.js"></script></head><body><div>    <input type="text" id="username">    <input type="password" id="password">    <input type="button" value="登录" id="loginBtn"></div><script>    $("#loginBtn").click(function () {        let _csrf = $.cookie('XSRF-TOKEN');        $.post('/login.html', {            username: $("#username").val(),            password: $("#password").val(),            _csrf: _csrf        }, function (data) {            alert(data);        })    })</script></body></html>
复制代码


区别:form 表单服务端将 CSRF 令牌保存到 HttpSession 中,ajax 请求是服务端将 CSRF 令牌放在 Cookie 中。

在 Cookie 上知道 SameSite 属性

通过在 Cookie 上指定 SameSite 属性,要求浏览器从外部站点发送请求时,不应携带 Cookie 信息,进而防止 CSRF 攻击。


SameSite 属性:


  • Strict:只有同一站点发送的请求才包含 Cookie 信息,不同站点发送的请求不会包含 Cookie 信息。

  • Lax:同一站点发送的请求或者导航到目标地址的 GET 请求会自动包含 Cookie 信息,否则不包含 Cookie 信息。

  • None:Cookie 将从所有上下文中发送,即允许跨域发送。


@Beanpublic CookieSerializer httpSessionIdResolver(){    DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();    cookieSerializer.setSameSite("strict");    return cookieSerializer;}
复制代码

CsrfFilter 过滤器

CsrfFilter 是 Spring Security 过滤器链中的一环,在过滤器中校验客户端传来的 CSRF 令牌是否有效。


CsrfFilter 过滤器是由 CsrfConfigurer 进行配置的,而 CsrfConfigurer 是在 WebSecurityConfigurerAdapter 的 getHttp 方法中添加进 HttpSecurity 中的。


CsrfFilter 继承 OncePerRequestFilter,最重要的方法就是 doFilterInternal 了


protected void doFilterInternal(HttpServletRequest request,      HttpServletResponse response, FilterChain filterChain)            throws ServletException, IOException {   request.setAttribute(HttpServletResponse.class.getName(), response);
CsrfToken csrfToken = this.tokenRepository.loadToken(request); final boolean missingToken = csrfToken == null; if (missingToken) { csrfToken = this.tokenRepository.generateToken(request); this.tokenRepository.saveToken(csrfToken, request, response); } request.setAttribute(CsrfToken.class.getName(), csrfToken); request.setAttribute(csrfToken.getParameterName(), csrfToken);
if (!this.requireCsrfProtectionMatcher.matches(request)) { filterChain.doFilter(request, response); return; }
String actualToken = request.getHeader(csrfToken.getHeaderName()); if (actualToken == null) { actualToken = request.getParameter(csrfToken.getParameterName()); } if (!csrfToken.getToken().equals(actualToken)) { if (this.logger.isDebugEnabled()) { this.logger.debug("Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request)); } if (missingToken) { this.accessDeniedHandler.handle(request, response, new MissingCsrfTokenException(actualToken)); } else { this.accessDeniedHandler.handle(request, response, new InvalidCsrfTokenException(csrfToken, actualToken)); } return; }
filterChain.doFilter(request, response);}
复制代码


  1. 首先调用 tokenRepository.loadToken 方法进行加载出 CsrfToken 对象 CsrfToken 为接口,用来描述 CSRF 令牌信息,默认 tokenRepository 对象类型为 LazyCsrfTokenRepository,CsrfTokenRepository 是 SpringSecurity 中提供的 CsrfToken 保存接口,实现类有 HttpSessionCsrfTokenRepository,CookieCsrfTokenRepository,LazyCsrfTokenRepository。HttpSessionCsrfTokenRepository 是将 CsrfToken 保存在 HttpSession 中,CookieCsrfTokenRepository 是将 CsrfToken 保存在 Cookie 中,LazyCsrfTokenRepository 是一个代理类,可以代理 HttpSessionCsrfTokenRepository 或者 CookieCsrfTokenRepository,代理目的是延迟保存生成的 CsrfToken。

  2. 如果 CsrfToken 对象不存在,则立马生成 CsrfToken 对象并保存起来。

  3. 将生成的 CsrfToken 对象设置到 request 属性中,这样我们在前端页面中就可以渲染出生成的令牌信息了。

  4. 调用 requireCsrfProtectionMatcher.matches 方法进行请求判断,该方法主要判断当前请求方法是否为 GET,HEAD,TRACE 以及 OPTIONS。如果当前请求方法是这四种之一,则请求直接过,不用进行 CSRF 的令牌校验,上一步没有必须进行保存 CsrfToken,LazyCsrfTokenRepository 生成了 CsrfToken 令牌没有立即保存,而是后面调用 getToken 时才保存。

  5. 如果请求不是 GET,HEAD,TRACE 以及 OPTIONS,先从请求头中提取出 CSRF 令牌,请求头没有,则从请求参数中提取出 CSRF 令牌,将拿到的 CSRF 令牌和第 1 步中通过 loadToken 加载出来的令牌进行对比,判断请求传来的 CSRF 令牌是否合法。


总结:请求到达后,会经过 CsrfFilter,在该过滤器中,首先加载出保存的 CsrfToken,可以是从 HttpSession 中加载,也可以是从请求头携带的 Cookie 中加载,默认是从 HttpSession 中加载,如果加载出来的 CsrfToken 为 null,则立即生成一个 CsrfToken 并保存起来,由于默认 tokenRepository 类型是 LazyCsrfTokenRepository,所以这里的保存并不是真正的保存,因为如果请求方法是 GET,HEAD,TRACE 以及 OPTIONS,就没有必要保存。然后将生成的 CsrfToken 放到请求对象中,方面前端渲染。然后判断请求方法是否是需要进行 CSRF 令牌校验的方法,如果不是,则直接执行后面的过滤器,否则就从请求中拿出 CSRF 令牌信息和一开始加载出来的令牌进行对比。

CsrfAuthenticationStrategy

CsrfAuthenticationStrategy 实现了 SessionAuthenticationStrategy 接口,默认也是由 CompositeSessionAuthenticationStrategy 代理执行,登录成功后触发执行,CsrfAuthenticationStrategy 主要用于在登录成功后,删除旧的 CsrfToken 并生成一个新的 CsrfToken


public void onAuthentication(Authentication authentication,      HttpServletRequest request, HttpServletResponse response)            throws SessionAuthenticationException {   boolean containsToken = this.csrfTokenRepository.loadToken(request) != null;   if (containsToken) {      this.csrfTokenRepository.saveToken(null, request, response);
CsrfToken newToken = this.csrfTokenRepository.generateToken(request); this.csrfTokenRepository.saveToken(newToken, request, response);
request.setAttribute(CsrfToken.class.getName(), newToken); request.setAttribute(newToken.getParameterName(), newToken); }}
复制代码


发布于: 刚刚阅读数: 3
用户头像

还未添加个人签名 2020.02.29 加入

公众号《盼盼小课堂》,多平台优质博主

评论

发布
暂无评论
CSRF_7月月更_周杰伦本人_InfoQ写作社区