一、跨站请求伪造(CSRF)概述
1.1 什么是 CSRF
跨站请求伪造(Cross-Site Request Forgery,简称 CSRF 或 XSRF),是一种迫使已登录的用户在不知情的情况下,执行非本意的操作请求攻击。攻击者通过精心构造的恶意请求,利用用户在目标网站已建立的信任关系,诱使用户浏览器发送恶意请求,从而达到篡改数据、执行非法操作等目的。与跨站脚本攻击(XSS)不同,CSRF 攻击利用的是用户的身份,而不是直接注入恶意脚本。
1.2 CSRF 攻击的原理
- 用户登录目标网站:用户在浏览器中登录银行、购物网站等目标站点,服务器验证身份后,向用户浏览器颁发会话凭证(如 Cookie、Token),以此识别用户身份,建立信任关系。
- 用户访问恶意网站:用户在未退出目标网站的情况下,访问了包含恶意请求的第三方网站。该恶意请求可以是一个隐藏的图片标签、自动提交的表单,或者是一段 JavaScript 代码发起的请求。
- 浏览器自动携带会话凭证:由于浏览器会自动在同源请求中携带会话凭证(如 Cookie),当恶意请求发送到目标网站时,服务器会误以为是用户的真实操作,从而执行恶意请求中的指令 。
1.3 CSRF 攻击的危害
- 账户资金被盗取:在金融类网站中,攻击者可构造转账、支付等恶意请求,利用用户身份转移资金。
- 个人信息被篡改:在社交平台、电商平台,可伪造修改用户个人信息的请求,造成信息泄露或被恶意修改。
- 非法操作执行:在办公系统中,可模拟用户执行删除文件、修改权限等操作,影响正常业务运行。
二、CSRF攻击案例
以下为你介绍一些跨站请求伪造(CSRF)的实战案例:
1. **论坛恶意发言案例**:假设在一个论坛`www.bbs.com`中,用户登录后可以发表言论。正常情况下,用户发言“hello”时发出的请求,会带上该域下的cookie用于身份验证。攻击者构造了一个恶意页面`csrf.html`,并将其放在自己的服务器`www.evil.com`上 。在这个恶意页面中,攻击者创建了一个发言的GET请求,链接为`http://www.bbs.com/talk.php?msg=goodbye` 。当已登录`www.bbs.com`的用户在未退出登录的情况下,被诱骗访问了`http://www.evil.com/csrf.html`,由于浏览器会自动携带`www.bbs.com`域下含有用户认证信息的cookie,此时就会按照攻击者的意愿提交一份内容为“goodbye”的发言,而用户对此并不知情。这看似是一个简单的恶作剧,但如果攻击者利用这种方式发布一些违法、违规或破坏论坛秩序的言论,就会给用户和论坛带来不良影响。
2. **网站管理员账号添加案例**:某网站的后台管理系统存在CSRF漏洞。攻击者构造了一个恶意页面`csrf2.html`,页面中包含一个添加管理员用户的请求,例如`http://www.targetsite.com/admin/addUser.php?username=evil&password=123456&role=admin` 。攻击者通过给已登录该网站后台的管理员发送钓鱼邮件、在管理员可能访问的其他页面植入恶意链接等方式,诱骗管理员访问`http://www.evil.com/csrf2.html`。当管理员访问该页面时,由于浏览器自动携带了管理员在目标网站的会话凭证(如cookie),服务器会误以为是管理员本人的操作,从而添加了一个用户名为“evil”的管理员用户。攻击者进而可以利用这个新添加的管理员账号,对网站进行一系列恶意操作,如篡改网站内容、删除重要数据、泄露用户信息等,严重影响网站的正常运营和用户数据安全。
3. **银行转账案例**:在银行网站的转账功能中,用户登录后进行转账操作,请求会携带用户身份验证信息。攻击者伪造一个转账请求,并将其嵌入到钓鱼网站页面中。比如钓鱼网站页面的代码中包含如下表单:
<form id="csrf - form" action="https://bank.example.com/transferMoney" method="post">
<input type="hidden" name="toAccount" value="attackerAccount">
<input type="hidden" name="amount" value="1000">
</form>
<script>
document.getElementById('csrf - form').submit();
</script>
当用户登录银行网站后,在未退出的情况下访问了该钓鱼网站,浏览器会自动提交这个转账请求,将1000元转账到攻击者账户,导致用户资金损失。银行系统由于接收到带有合法用户会话凭证的请求,无法辨别该请求是被伪造的,从而执行了非法的转账操作。 4. **社交平台信息篡改案例**:以一个社交平台为例,用户登录后可以修改自己的个人信息,如头像、简介等。攻击者创建一个恶意网页,页面中包含伪造的修改用户信息请求。假设修改简介的正常请求为`https://socialplatform.com/updateProfile.php?bio=新的简介内容`,攻击者构造的恶意请求可能为`https://socialplatform.com/updateProfile.php?bio=攻击者植入的恶意广告或不良信息` 。攻击者通过发送恶意链接给已登录社交平台的用户,一旦用户点击链接访问了该恶意网页,浏览器会自动发送伪造请求到社交平台服务器。由于请求携带了用户的有效会话凭证,服务器会将用户的简介修改为攻击者设置的内容,侵犯用户隐私,影响用户在社交平台的形象,并且如果植入的是恶意广告,还可能进一步引导其他用户陷入其他安全风险。
三、Java Web 应用中的 CSRF 漏洞代码示例
2.1 存在 CSRF 漏洞的转账功能 Servlet 代码
@WebServlet("/transferMoney")
public class TransferMoneyServlet extends HttpServlet {
private AccountService accountService = new AccountService();
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 未进行任何CSRF防护,直接获取参数执行转账
String toAccount = request.getParameter("toAccount");
double amount = Double.parseDouble(request.getParameter("amount"));
// 假设通过会话获取当前用户信息
HttpSession session = request.getSession(false);
String currentUser = (String) session.getAttribute("username");
accountService.transferMoney(currentUser, toAccount, amount);
response.getWriter().println("转账成功");
}
}
2.2 恶意网站构造的 CSRF 攻击页面
<!DOCTYPE html>
<html>
<body>
<form id="csrf-form" action="https://example.com/transferMoney" method="post">
<input type="hidden" name="toAccount" value="attackerAccount">
<input type="hidden" name="amount" value="1000">
</form>
<script>
document.getElementById('csrf-form').submit();
</script>
</body>
</html>
当已登录银行网站的用户访问该恶意页面时,浏览器会自动提交转账请求,将 1000 元转账到攻击者账户,而用户对此毫不知情。
四、Java Web 应用中 CSRF 攻击的防御方案
3.1 验证来源站点(Referer 头检查)
服务器通过检查请求中的Referer头信息,判断请求是否来自合法的站点。合法请求的Referer头应指向目标网站的页面,而恶意请求的Referer通常指向恶意站点。
@WebServlet("/transferMoney")
public class TransferMoneyServlet extends HttpServlet {
private AccountService accountService = new AccountService();
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String referer = request.getHeader("Referer");
if (referer == null ||!referer.startsWith("https://example.com")) {
response.getWriter().println("非法请求");
return;
}
String toAccount = request.getParameter("toAccount");
double amount = Double.parseDouble(request.getParameter("amount"));
HttpSession session = request.getSession(false);
String currentUser = (String) session.getAttribute("username");
accountService.transferMoney(currentUser, toAccount, amount);
response.getWriter().println("转账成功");
}
}
但这种方法存在局限性,部分浏览器出于隐私保护不会发送Referer头,且攻击者也可能伪造Referer头,因此不能作为唯一的防护手段。
3.2 使用 CSRF Token
在用户访问页面时,服务器生成一个随机的 Token,并将其存储在用户会话(Session)中,同时通过隐藏表单字段或 HTTP 头的方式传递给客户端。客户端在提交请求时,必须携带该 Token,服务器收到请求后,验证 Token 是否与会话中存储的一致,若不一致则拒绝请求。
生成 Token 并传递给页面
@WebServlet("/transferPage")
public class TransferPageServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String csrfToken = generateCsrfToken();
request.getSession().setAttribute("csrfToken", csrfToken);
request.setAttribute("csrfToken", csrfToken);
request.getRequestDispatcher("transfer.jsp").forward(request, response);
}
private String generateCsrfToken() {
// 生成随机Token的逻辑,可使用UUID等方式
return UUID.randomUUID().toString();
}
}
页面中包含 Token
<!DOCTYPE html>
<html>
<body>
<form action="transferMoney" method="post">
<input type="hidden" name="csrfToken" value="${csrfToken}">
<label for="toAccount">转入账户:</label>
<input type="text" id="toAccount" name="toAccount"><br>
<label for="amount">转账金额:</label>
<input type="text" id="amount" name="amount"><br>
<input type="submit" value="转账">
</form>
</body>
</html>
服务器验证 Token
@WebServlet("/transferMoney")
public class TransferMoneyServlet extends HttpServlet {
private AccountService accountService = new AccountService();
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String csrfTokenFromRequest = request.getParameter("csrfToken");
String csrfTokenFromSession = (String) request.getSession().getAttribute("csrfToken");
if (csrfTokenFromRequest == null ||!csrfTokenFromRequest.equals(csrfTokenFromSession)) {
response.getWriter().println("非法请求");
return;
}
String toAccount = request.getParameter("toAccount");
double amount = Double.parseDouble(request.getParameter("amount"));
HttpSession session = request.getSession(false);
String currentUser = (String) session.getAttribute("username");
accountService.transferMoney(currentUser, toAccount, amount);
response.getWriter().println("转账成功");
}
}
3.3 双重 Cookie 验证
在用户登录时,服务器生成一个随机的 Token,并将其存储在 Cookie 中,同时通过其他方式(如隐藏表单字段)传递给客户端。客户端在提交请求时,将 Cookie 中的 Token 取出,放在请求参数中一起发送给服务器。服务器验证 Cookie 中的 Token 与请求参数中的 Token 是否一致,只有两者一致时才处理请求。
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String username = request.getParameter("username");
String password = request.getParameter("password");
// 验证用户名密码逻辑
String csrfToken = generateCsrfToken();
Cookie csrfCookie = new Cookie("csrfToken", csrfToken);
response.addCookie(csrfCookie);
request.getSession().setAttribute("csrfToken", csrfToken);
response.sendRedirect("home.jsp");
}
private String generateCsrfToken() {
return UUID.randomUUID().toString();
}
}
------------------
@WebServlet("/transferMoney")
public class TransferMoneyServlet extends HttpServlet {
private AccountService accountService = new AccountService();
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String csrfTokenFromRequest = request.getParameter("csrfToken");
Cookie[] cookies = request.getCookies();
String csrfTokenFromCookie = null;
if (cookies != null) {
for (Cookie cookie : cookies) {
if ("csrfToken".equals(cookie.getName())) {
csrfTokenFromCookie = cookie.getValue();
break;
}
}
}
if (csrfTokenFromRequest == null || csrfTokenFromCookie == null ||
!csrfTokenFromRequest.equals(csrfTokenFromCookie)) {
response.getWriter().println("非法请求");
return;
}
String toAccount = request.getParameter("toAccount");
double amount = Double.parseDouble(request.getParameter("amount"));
HttpSession session = request.getSession(false);
String currentUser = (String) session.getAttribute("username");
accountService.transferMoney(currentUser, toAccount, amount);
response.getWriter().println("转账成功");
}
}
五、CSRF 防御最佳实践
4.1 建立多层防御体系
将上述多种防御方法结合使用,如同时采用 CSRF Token 和双重 Cookie 验证,即使一种方法被突破,其他方法仍能提供防护。同时,定期检查Referer头信息,辅助判断请求来源的合法性。
4.2 安全编码规范
- 对所有涉及用户操作的请求(如 POST、PUT、DELETE 请求)都进行 CSRF 防护,避免遗漏。
- 确保 Token 的生成具有足够的随机性和唯一性,防止被猜测或破解。
- 及时更新会话中的 Token,避免 Token 被长期使用而增加泄露风险。
4.3 安全测试与监控
使用专业的安全测试工具(如 OWASP ZAP、Burp Suite)对应用进行 CSRF 漏洞扫描,模拟攻击场景检测系统的防护能力。同时,在服务器端建立日志监控机制,对异常请求进行记录和分析,及时发现潜在的 CSRF 攻击行为并采取应对措施。
以上详细介绍了 CSRF 攻击与防御。若你对代码细节、其他防御手段还有疑问,或者想了解更多网络安全知识,欢迎随时告诉我。