1 CSRF漏洞
1.1 漏洞原理
跨站请求伪造(Cross-site request forgery)CSRF,是一种使已登录用户在不知情的情况下执行某种动作的攻击。因为攻击者看不到伪造请求的响应结果,所以CSRF攻击主要用来执行动作,而非窃取用户数据。 当受害者是一个普通用户时,CSRF可以实现在其不知情的情况下转移用户资金、发送邮件等操作;但是如果受害者是一个具有管理员权限的用户时CSRF则可能威胁到整个Web系统的安全。
CSRF攻击攻击原理及过程如下:
-
用户user打开浏览器,访问受信任网站A,输入用户名和密码请求登录网站A;
-
在用户信息通过验证后,网站A产生Cookie信息并返回给浏览器,此时用户登录网站A成功,可以正常发送请求到网站A;
-
用户未退出网站A之前,在同一浏览器中,打开一个TAB页访问网站B;
-
网站B接收到用户请求后,返回一些攻击性代码,并发出一个请求要求访问第三方站点 A;
-
浏览器在接收到这些攻击性代码后,根据网站B的请求,在用户不知情的情况下携带 Cookie信息,向网站A发出请求。网站A并不知道该请求其实是由B发起的,所以会根据用户C的 Cookie信息以C的权限处理该请求,导致来自网站B的恶意代码被执行。
1.2 常见存在场景及漏洞审计点
CSRF 可能出现的场景: 更改个人信息 添加/修改资料 关注/取关用户 发布主题或信息 与交易相关的操作
常见漏洞点:
代码审计中的思路是检查是否校验Referer、是否给cookie设置SameSite属性、敏感操作是否会生成
CSRF token,如果都不存在再查看请求参数中是否存在不可被攻击者猜测的字段,比如验证码等参数。
例如: 后台修改密码的代码即存在 CSRF 攻击风险:
@RequestMapping("/doUpdate") public String doUpdate(HttpServletRequest request, Users user, Model model) { HttpSession session = request.getSession(); user.setUsername((String)session.getAttribute("user")); System.out.println(session.getAttribute("user")); usersService.update(user); // 更新用户信息 model.addAttribute("success", "修改成功"); return "sqli/update"; }
攻击者可在其服务器上创建钓鱼页面,其中包含构造的发送修改用户密码请求的代码, 当用户在已登录网站的情况下点击攻击者发送的钓鱼链接时,密码将被修改。
例如: 如下是一段更新个人信息的代码,存在CSRF漏洞。
String email=request.getParameter("email"); String tel=request.getParameter("password"); String realname=request.getParameter("realname"); Object[] params = new Object[4]; params[0] = email; params[1] = password; params[2] = realname;params[3] = userid; final String sql = "update user set email=?,password=?,realname=? where userid=?"; conn.execUpdate(sql,params);
在代码中,攻击者可在恶意站点中构造如下表单,诱使登陆者点击。
<script> document.form1.submit(); </script> <div style="display:'none'"> <form name="form1" action="http://A.com/modify.jsp" method="POST"> <input name="email" value="test@test.com"> <input name="password" value="test"> <input name="realname" value="test"> <input type="submit"> </form> </div>
例如:Referer校验不严:
// 定义一个名为RefererInterceptor的类,该类继承自HandlerInterceptorAdapter接口 public class RefererInterceptor extends HandlerInterceptorAdapter { // 定义一个私有布尔变量check,默认值为true // 如果check为true,则执行preHandle方法中的逻辑 private Boolean check = true; // 重写HandlerInterceptorAdapter中的preHandle方法 // 此方法会在请求处理之前被调用 @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 如果check为false,则直接返回true,继续执行后续流程 if (!check) { return true; } // 获取请求头中的Referer信息 String referer = request.getHeader("Referer"); // 如果Referer不为空,并且以"www.testdomain.com"开头 if ((referer != null) && (referer.trim().startsWith("www.testdomain.com"))) { // 继续执行后续的请求处理流程(包括其他Interceptor和Controller) chain.doFilter(request, response); } else { // 如果Referer不符合条件,则重定向到"index.jsp"页面 request.getRequestDispatcher("index.jsp").forward(request, response); } // 通常在这里返回false,表示当前Interceptor已经处理完请求,并且不继续执行后续的Interceptor或Controller // 注意:如果上面的chain.doFilter(request, response)被调用,那么返回false可能会导致请求被截断 // 根据实际情况,可能需要在这里返回true或false return false; } }
1.3 修复方案
Referer校验,对HTTP请求的Referer校验,如果请求Referer的地址不在允许的列表中,则拦截请求。Token校验,服务端生成随机token,并保存在本次会话cookie中,用户发起请求时 附带token参数,服务端对该随机数进行校验。如果不正确则认为该请求为伪造请求 拒绝该请求。
对于高安全性操作则可使用验证码、短信、密码等二次校验措施 如下给出在 SpringBoot 项目中,使用 Spring-Security 组件来进行 Token 校验的示例
代码:
-
首先注册过滤器如下,本示例代码中使用的过滤器为Spring-Security提供的HttpSessionCsrfTokenRepository 过滤器
@Configuration public class CSRFFilterConfig { public FilterRegistrationBean csrfFilterRegistrationBean() { FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(); // 使用 HttpSessionCsrfTokenRepository 过滤器进行 token 校验时, token值存储在session中 filterRegistrationBean.setFilter(new CsrfFilter(new HttpSessionCsrfTokenRepository())); filterRegistrationBean.addUrlPatterns("/*"); return filterRegistrationBean; } }
-
使用${csrf.parameterName}和${csrf.token}表达式分别获取Token参数名和 Token 参数值,并记录到 hidden 字段中
<div class="layui-form-item"> <label class="layui-form-label">新密码:</label> <div class="layui-input-block"> <input type="text" name="password" class="layui-input"/> </div> </div> <div class="layui-form-item"> <div class="layui-input-block"> <button class="layui-btn" lay-submit lay-filter="demo1">修改 </button> 1</div> </div> <!-- 存放 token--> <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>