CSRF漏洞安全
# CSRF漏洞介绍
- CSRF全称:Cross—Site Request Forgery
- 中文名称:跨站请求伪造
- 概念:一般来说,一种可以被攻击者用来通过用户浏览器冒充用户身份向服务器发送伪造请求并被
目标服务器成功执行的漏洞被称之为CSRF漏洞。
# 案例
一个银行资金被盗的案例
- 徐先生登录网银系统
- 点击不明链接
- 银行账户资金被盗
受害者 Bob 在银行有一笔存款,通过对银行的网站发送请求 http://bank.example/withdraw?account (opens new window)
=bob&amount=1000000&for=bob2 可以使 Bob 把 1000000 的存款转到 bob2 的账号下。通常情况
下,该请求发送到网站后,服务器会先验证该请求是否来自一个合法的 session,并且该 session 的用
户 Bob 已经成功登陆。
黑客 Mallory 自己在该银行也有账户,他知道上文中的 URL 可以把钱进行转帐操作。Mallory 可以自己
发送一个请求给银行:http://bank.example/withdraw?account=bob&amount=1000000&for=Mallo (opens new window)
ry。但是这个请求来自 Mallory 而非 Bob,他不能通过安全认证,因此该请求不会起作用。
这时,Mallory 想到使用 CSRF 的攻击方式,他先自己做一个网站,在网站中放入如下代码: src=”htt
p://bank.example/withdraw?account=bob&amount=1000000&for=Mallory ”,并且通过广告等诱使
Bob 来访问他的网站。
当 Bob 访问该网站时,上述 url 就会从 Bob 的浏览器发向银行,而这个请求会附带 Bob 浏览器中的
cookie 一起发向银行服务器。大多数情况下,该请求会失败,因为他要求 Bob 的认证信息。但是,如
果 Bob 当时恰巧刚访问他的银行后不久,他的浏览器与银行网站之间的 session 尚未过期,浏览器的
cookie 之中含有 Bob 的认证信息。这时,悲剧发生了,这个 url 请求就会得到响应,钱将从 Bob 的账
号转移到 Mallory 的账号,而 Bob 当时毫不知情。等以后 Bob 发现账户钱少了,即使他去银行查询日
志,他也只能发现确实有一个来自于他本人的合法请求转移了资金,没有任何被攻击的痕迹。而
Mallory 则可以拿到钱后逍遥法外。
示例:银行网站 A,它以 GET 请求来完成银行转账的操作,如:
http://www.mybank.com/Transfer.php?toBankId=11&money=1000
危险网站 B,它里面有一段 HTML 的代码如下:
<img src=http://www.mybank.com/Transfer.php?toBankId=11&money=1000>
如下:其中Web A为存在CSRF漏洞的银行网站,Web B为攻击者构建的恶意网站,User C为银行网站
的合法用户。
- 用户C打开浏览器,访问受信任网站A,输入用户名和密码请求登录网站A;
- 在用户信息通过验证后,网站A产生Cookie信息并返回给浏览器,此时用户登录网站A成功,可以
正常发送请求到网站A; - 用户未退出网站A之前,在同一浏览器中,打开一个TAB页访问网站B;
- 网站B接收到用户请求后,返回一些攻击性代码,并发出一个请求要求访问第三方站点A;
# CSRF漏洞成因分析

成因有几点:
- 浏览器带着cookie去访问恶意网站,这点源于用户的不良使用习惯,未退出信任网站A的登录去访问其他网站,这单没办法做限制,只能给个友好提示,不要随意点击未知网站之类的
- 服务器没有区分是否为伪造请求
- 服务器没有做二次验证
# 攻击代码示例
GET 请求:
<img src="http://www.study.com/admin/resetPassword?id=1" />
<iframe src="http://www.study.com/admin/resetPassword?id=1"
style='display:none'></iframe>
2
3
POST 请求:
隐藏表单、自动提交:页面跳转问题 把上述功能通过iframe引入新页面
<iframe src="form.html" style='display:none'></iframe>
# CSRF漏洞危害分析
攻击难度大于XSS漏洞:
- 对目标网站接口有了解(这点很重要)
- 找到攻击目标人群
- 构造恶意网站或利用第三方有XSS漏洞的网站
危害大于XSS漏洞:
- 修改密码
- 网银转账
- 创建后台用户
- 调整账户余额
- 调整商品余额
- 权限角色调整
- 发布信息/垃圾短信
- 下单购买
- 关注(圈粉)
- 数字货币交易
但实际上CSRF危害的上限不仅限于这些,只要对目标网站的接口足够了解,所有页面请求都可能被利用,而且攻击者不需要看到响应,只需要伪造的请求可以以合法的身份发送即可,网站管理员、网站用户都有可能成为攻击目标。
# CSRF漏洞预防策略
CSRF漏洞能够通过以下三点预防:
- referer校验
- 动态token验证
- 业务二次校验
# referer校验
referer是什么?
HTTP Referer是header的一部分,当浏览器向web服务器发送请求的时候,一般会带上Referer,告诉 服务器该网页是从哪个页面链接过来的,服务器因此可以获得一些信息用于处理。
HTTP头中的Referer字段记录了该 HTTP 请求的来源地址。在通常情况下,访问一个安全受限页面的请 求自于同一个网站,而如果黑客要对其实施 CSRF攻击,他一般只能在他自己的网站构造请求。因此, 可以通过验证Referer值来防御CSRF 攻击。 
受限页面添加referer校验
@RequestMapping("/resetPassword")
@ResponseBody
public String resetPassword(@NotNull Integer id, HttpServletRequest request) {
boolean isLogin = validateLogin();
if (!isLogin) {
return "请先登录后台!";
}
// 校验
String referrer = request.getHeader("referer");
logger.debug("referrer:{}", referrer);
StringBuilder sb = new StringBuilder();
sb.append(request.getScheme()).append("://").append(request.getServerName());
logger.debug("basePath:{}", sb.toString());
if (referrer == null || referrer.equals("") ||
!referrer.startsWith(sb.toString())) {
return "非法访问,请通过页面正常访问!";
}
if (systemUserService.resetPassword(id, "123456") > 0) {
return "密码重置为123456!";
} else {
return "密码重置失败";
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
可以将校验代码抽取到拦截器中,或是针对某个受限接口添加
String referrer = request.getHeader("referer");
logger.debug("referrer:{}", referrer);
StringBuilder sb = new StringBuilder();
sb.append(request.getScheme()).append("://").append(request.getServerName());
logger.debug("basePath:{}", sb.toString());
if (referrer == null || referrer.equals("") ||
!referrer.startsWith(sb.toString())) {
return "非法访问,请通过页面正常访问!";
}
2
3
4
5
6
7
8
9
**※※※缺点:**不能防CSRF+XSS的场景,因为XSS是本网站内页面发起的请求,referer为合法地址
# 动态token验证
CSRF漏洞的典型缺陷
- CSRF只能发送固定参数的请求
- 不能读取页面响应数据
动态token原理
- token由服务端随机生成,保存在session中
- 浏览器每次请求都需要携带token
- 服务器端校验token是否正确
- 每次响应都更新token
CSRF 攻击之所以能够成功,是因为黑客可以完全伪造用户的请求,该请求中所有的用户验证信息都是 存在于cookie中,因此黑客可以在不知道这些验证信息的情况下直接利用用户自己的cookie 来通过安全验证。要抵御CSRF,关键在于在请求中放入黑客所不能伪造的信息,并且该信息不存在于 cookie 之中。可以在 HTTP 请求中以参数的形式加入一个随机产生的 token,并在服务器端建立一个拦截器来验证这个 token,如果请求中没有token或者 token 内容不正确,则认为可能是 CSRF 攻击而拒绝该请求。这种方法要比检查 Referer 要安全一些,token 可以在用户登陆后产生并放于session之中,然后在每次请求时把token 从 session 中拿出,与请求中的 token 进行比对,但这种方法的难点在于如何把 token 以参数的形式加入请求。 对于 GET 请求,token 将附在请求地址之后,这样 URL 就变成 http://url?csrftoken=tokenvalue。 而对于 POST 请求来说,要在 form 的最后加上 ,这样就把token 以参数的形式加入请求了。
# http请求参数实现演示
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import sun.misc.BASE64Encoder;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Random;
/**
* @author liujunkai
* @date 2022/12/7 15:49
*/
public class CsrfTokenInterceptor implements HandlerInterceptor {
static final Logger logger = LogManager.getLogger(CsrfTokenInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
logger.debug("token拦截器拦截到对:{}的访问", request.getRequestURI());
String token = request.getParameter("csrf_token");
logger.debug("token:{}", token);
HttpSession session = request.getSession();
Object tokenInSession = session.getAttribute("csrf_token");
if (request.getServletPath().equals("/admin/login")) {
//登录页面放行,第一次生成csrf-token
logger.debug("token拦截器:{}", "访问后台登录页不校验token");
return true;
}
if (token == null || token == "" || tokenInSession == null || !
((String) tokenInSession).equals(token)) {
response.setContentType("text/plain; charset=utf-8");
response.getWriter().write("token无效,非法访问,请通过页面正常访问!");
return false;
}
return true;
}
/**
* 生成Token
* Token:Nv6RRuGEVvmGjB+jimI/gw==
* @return
*/
public String makeToken() {
String token = (System.currentTimeMillis() + new Random().nextInt(999999999)) + "";
//数据指纹 128位长 16个字节 md5
try {
MessageDigest md = MessageDigest.getInstance("md5");
byte md5[] = md.digest(token.getBytes());
//base64编码--任意二进制编码明文字符
BASE64Encoder encoder = new BASE64Encoder();
return encoder.encode(md5);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
//生成随机字符串,放入session中,供下次使用
request.getSession().setAttribute("csrf_token", makeToken());
//将token值放入modelmap,供页面使用
modelAndView.getModelMap().put("csrf_token", request.getSession().getAttribute("csrf_token"));
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
**※缺点:**可能会导致页面不支持刷新,刷新完会报token过期,所以可以考虑部分页面做校验,或者是GET请求不做校验,仅对POST请求做校验
# http请求cookie+加密参数实现
(如果是不希望黑客访问html可以采用这种方式)
html,不是JSP,并不能动态的从session中取出csrf_token值。只能采用加密的方式了。
这可能是最简单的解决方案了,因为攻击者不能获得第三方的Cookie(理论上),所以表单中的数据也就构造失败了。
我采用的hash加密方法是JS实现Java的HashCode方法,得到hash值,这个比较简单。也可以采用其他的hash算法。
前端向后台传递hash之后的csrf_token值和cookie中的csrf_token值,后台拿到cookie中的csrf_token 值后得到hashCode值然后与前端传过来的值进行比较,一样则通过。
# http请求header实现(推荐)
这种方法也是使用 token 并进行验证,和上一种方法不同的是,这里并不是把 token 以参数的形式置于 HTTP 请求之中,而是把它放到 HTTP 头中自定义的属性里。通过 XMLHttpRequest 这个类,可以一次性给所有该类请求加上 csrftoken 这个 HTTP 头属性,并把 token 值放入其中。这样解决了上种方法在请求中加入 token 的不便,同时,通过 XMLHttpRequest 请求的地址不会被记录到浏览器的地址栏,也不用担心 token 会透过Referer 泄露到其他网站中去。
# 业务二次校验
- 修改密码,需输入原密码
- 交易系统设置交易密码
- 增加图形验证码校验
- 网银转账短信验证码
# 常用检测方法
- 人工校验
- CSRFTester
- 下载地址:https://www.owasp.org/index.php/File:CSRFTester-1.0.zip
- 使用参考:https://blog.csdn.net/ShiMengRan107/article/details/70238669
检测CSRF漏洞是一项比较繁琐的工作,最简单的方法就是抓取一个正常请求的数据包,去掉Referer字 段后再重新提交,如果该提交还有效,那么基本上可以确定存在CSRF漏洞。
随着对CSRF漏洞研究的不断深入,不断涌现出一些专门针对CSRF漏洞进行检测的工具,如 CSRFTester,CSRF Request Builder等。
以CSRFTester工具为例,CSRF漏洞检测工具的测试原理如下:使用CSRFTester进行测试时,首先需要 抓取我们在浏览器中访问过的所有链接以及所有的表单等信息,然后通过在CSRFTester中修改相应的表 单等信息,重新提交,这相当于一次伪造客户端请求。如果修改后的测试请求成功被网站服务器接受, 则说明存在CSRF漏洞,当然此款工具也可以被用来进行CSRF攻击。