修复J2EE漏洞:4. CSRF漏洞
字数 1041 2025-08-29 08:31:53
J2EE CSRF漏洞防护详解
1. CSRF漏洞概述
CSRF(Cross-Site Request Forgery)跨站请求伪造是一种常见的Web安全漏洞,攻击者诱导受害者进入第三方网站,在受害者已登录目标网站的情况下,利用受害者的身份凭证执行非预期的操作。
2. Struts框架CSRF防护
2.1 使用<s:token/>标签
在Struts框架中,可以通过<s:token/>标签生成并验证CSRF令牌:
<s:form action="reg" theme="simple">
username:<s:textfield name="username"></s:textfield><br />
password:<s:password name="password"></s:password><br />
<s:token></s:token>
<s:submit value="注册"></s:submit>
</s:form>
2.2 配置token拦截器
在struts.xml中配置token拦截器:
<package name="test" namespace="/test" extends="struts-default">
<interceptors>
<!-- 配置拦截器 -->
<interceptor-stack name="tokenStack">
<!-- 命名拦截器栈,名字随便 -->
<interceptor-ref name="token"/>
<!-- 此拦截器为token拦截器,struts已经实现 -->
<interceptor-ref name="defaultStack" />
<!-- 默认拦截器,注意顺序,默认拦截器放在最下面 -->
</interceptor-stack>
</interceptors>
<default-interceptor-ref name="tokenStack" />
<!-- 让该包中所有action都是用我们配置的拦截器栈,名字和上面的对应 -->
</package>
2.3 流程说明
- 客户端申请token
- 服务器端生成token,并存放在session中,同时将token发送到客户端
- 客户端存储token,在请求提交时,同时发送token信息
- 服务器端统一拦截同一个用户的所有请求,验证当前请求是否需要被验证
- 验证session中token是否和用户请求中的token一致,如果一致则放行
- session清除会话中的token,为下一次的token生成作准备
- 并发重复请求到来,验证token和请求token不一致,请求被拒绝
3. SpringMVC框架CSRF防护
3.1 基本思路
- 跳转页面前生成随机token,并存放在session中
- form中将token放在隐藏域中,保存时将token放头部一起提交
- 获取头部token,与session中的token比较,一致则通过
- 生成新的token,并传给前端
3.2 配置拦截器
<mvc:interceptors>
<!-- csrf攻击防御 -->
<mvc:interceptor>
<!-- 需拦截的地址 -->
<mvc:mapping path="/**"/>
<!-- 需排除拦截的地址 -->
<mvc:exclude-mapping path="/resources/**"/>
<bean class="com.cnpc.framework.interceptor.CSRFInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
3.3 拦截器实现
public class CSRFInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
VerifyCSRFToken verifyCSRFToken = method.getAnnotation(VerifyCSRFToken.class);
// 如果配置了校验csrf token校验,则校验
if (verifyCSRFToken != null) {
// 是否为Ajax标志
String xrq = request.getHeader("X-Requested-With");
// 非法的跨站请求校验
if (verifyCSRFToken.verify() && !verifyCSRFToken(request)) {
if (StrUtil.isEmpty(xrq)) {
// form表单提交,url get方式,刷新csrftoken并跳转提示页面
String csrftoken = CSRFTokenUtil.generate(request);
request.getSession().setAttribute("CSRFToken", csrftoken);
response.setContentType("application/json;charset=UTF-8");
PrintWriter out = response.getWriter();
out.print("非法请求");
response.flushBuffer();
return false;
} else {
// 刷新CSRFToken,返回错误码,用于ajax处理,可自定义
String csrftoken = CSRFTokenUtil.generate(request);
request.getSession().setAttribute("CSRFToken", csrftoken);
ResultCode rc = CodeConstant.CSRF_ERROR;
response.setContentType("application/json;charset=UTF-8");
PrintWriter out = response.getWriter();
out.print(JSONObject.toJSONString(rc));
response.flushBuffer();
return false;
}
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// 第一次生成token
if (modelAndView != null) {
if (request.getSession(false) == null || StrUtil.isEmpty((String) request.getSession(false).getAttribute("CSRFToken"))) {
request.getSession().setAttribute("CSRFToken", CSRFTokenUtil.generate(request));
return;
}
}
// 刷新token
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
RefreshCSRFToken refreshAnnotation = method.getAnnotation(RefreshCSRFToken.class);
// 跳转到一个新页面 刷新token
String xrq = request.getHeader("X-Requested-With");
if (refreshAnnotation != null && refreshAnnotation.refresh() && StrUtil.isEmpty(xrq)) {
request.getSession().setAttribute("CSRFToken", CSRFTokenUtil.generate(request));
return;
}
// 校验成功 刷新token 可以防止重复提交
VerifyCSRFToken verifyAnnotation = method.getAnnotation(VerifyCSRFToken.class);
if (verifyAnnotation != null) {
if (verifyAnnotation.verify()) {
if (StrUtil.isEmpty(xrq)) {
request.getSession().setAttribute("CSRFToken", CSRFTokenUtil.generate(request));
} else {
Map<String, String> map = new HashMap<String, String>();
map.put("CSRFToken", CSRFTokenUtil.generate(request));
response.setContentType("application/json;charset=UTF-8");
OutputStream out = response.getOutputStream();
out.write((",'csrf':" + JSONObject.toJSONString(map)).getBytes("UTF-8"));
}
}
}
}
/**
* 处理跨站请求伪造 针对需要登录后才能处理的请求,验证CSRFToken校验
*/
protected boolean verifyCSRFToken(HttpServletRequest request) {
// 请求中的CSRFToken
String requstCSRFToken = request.getHeader("CSRFToken");
if (StrUtil.isEmpty(requstCSRFToken)) {
return false;
}
String sessionCSRFToken = (String) request.getSession().getAttribute("CSRFToken");
if (StrUtil.isEmpty(sessionCSRFToken)) {
return false;
}
return requstCSRFToken.equals(sessionCSRFToken);
}
}
4. ESAPI实现CSRF防护
4.1 生成CSRF Token
private String csrfToken = resetCSRFToken();
private String resetCSRFToken() {
String csrfToken = ESAPI.randomizer().getRandomString(8, DefaultEncoder.CHAR_ALPHANUMERICS);
return csrfToken;
}
4.2 在表单中添加CSRF Token
对于需要保护的表单,增加一个隐藏的csrfToken字段:
<!-- URL上 -->
<a href='<%=ESAPI.httpUtilities().addCSRFToken("/zhutougg/main?function=listUser")%>'>查询用户</a>
<!-- 表单中 -->
<input type="hidden" name="ctoken" value="<%=ESAPI.DefaultHTTPUtilities.getCSRFToken() %>">
4.3 服务器端验证
@RequestMapping(value="/admin/login",method = RequestMethod.POST)
public String listUser(HttpServletRequest request, HttpServletResponse response, Model model) {
ESAPI.httpUtilities().verifyCSRFToken(request);
// 其他业务逻辑
}
4.4 用户退出时移除token
ESAPI.authenticator().getCurrentUser().logout();
5. 最佳实践总结
- 重要操作必须使用POST请求:GET请求容易被利用进行CSRF攻击
- 使用CSRF Token:为每个表单生成唯一的token,并在服务器端验证
- 验证Referer头:检查请求来源是否合法
- 设置SameSite Cookie属性:限制第三方网站使用Cookie
- 双重提交Cookie:将token同时放在Cookie和请求参数中,服务器验证两者是否一致
- 定期更换Token:防止token被窃取后长期有效
- 敏感操作二次验证:如密码修改等操作要求用户重新输入密码
通过以上措施,可以有效防止CSRF攻击,保护Web应用的安全。