跨站请求伪造(CSRF)全面解析:原理、Java 代码示例与防御方案​

一、跨站请求伪造(CSRF)概述​

1.1 什么是 CSRF​

跨站请求伪造(Cross-Site Request Forgery,简称 CSRF 或 XSRF),是一种迫使已登录的用户在不知情的情况下,执行非本意的操作请求攻击。攻击者通过精心构造的恶意请求,利用用户在目标网站已建立的信任关系,诱使用户浏览器发送恶意请求,从而达到篡改数据、执行非法操作等目的。与跨站脚本攻击(XSS)不同,CSRF 攻击利用的是用户的身份,而不是直接注入恶意脚本。​

1.2 CSRF 攻击的原理​

  1. 用户登录目标网站:用户在浏览器中登录银行、购物网站等目标站点,服务器验证身份后,向用户浏览器颁发会话凭证(如 Cookie、Token),以此识别用户身份,建立信任关系。​
  1. 用户访问恶意网站:用户在未退出目标网站的情况下,访问了包含恶意请求的第三方网站。该恶意请求可以是一个隐藏的图片标签、自动提交的表单,或者是一段 JavaScript 代码发起的请求。​
  1. 浏览器自动携带会话凭证:由于浏览器会自动在同源请求中携带会话凭证(如 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 攻击与防御。若你对代码细节、其他防御手段还有疑问,或者想了解更多网络安全知识,欢迎随时告诉我。​

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值