文章总结: Thearticleexploresfront-endsecurityvulnerabilitiesXSSandCSRF.XSSallowsattackerstoinjectmaliciousscriptstostealsessionsandhijackusers,preventableviastrictinputfiltering.CSRFforgesrequestsusingauser’sestablishedcredentials,mitigatedbyaddingrandomTokenverificationtostate-changingrequests.Thetextprovidesactionabletechnicalguidance,includingJavafiltercodeandconfigurationsteps,tohelpdevelopersimplementdefensesagainstthesehighandmedium-riskthreats,ensuringrobustprotectionforwebapplicationinteractions. 综合评分: 91 文章分类: WEB安全,漏洞分析,安全开发,解决方案,应用安全
前端藏刀!XSS 与 CSRF 的攻防博弈——Web 应用安全实战(PART 4)
原创
耶度
野猪与安全
2025年12月26日 10:00 广东
各位开发同学,咱们的 Web 安全实战系列已经更新到第四篇啦!前几期我们陆续拆解了登录环节的核心漏洞——从可预测的凭证、错误消息枚举,到明文传输的登录请求和致命的 SQL 注入,每一个漏洞都藏着黑客入侵的突破口。
今天这篇,我们把焦点转向前端交互层。很多时候,我们以为加固了后端逻辑就万事大吉,却忽略了前端页面里可能藏着的“暗箭”——跨站点脚本攻击(XSS)和跨站点请求伪造(CSRF)。这两种漏洞专攻用户会话和身份盗用,隐蔽性极强,一不小心就会让用户和系统栽大跟头。话不多说,直接上干货!
跨站点脚本攻击(XSS)—— 页面里藏着的恶意代码
高风险漏洞
1. 漏洞描述:给网页“下毒”,用户浏览即中招
XSS(全称Cross Site Script,跨站脚本攻击),简单说就是黑客往Web页面里偷偷插入恶意 HTML 或 JavaScript 代码。当用户正常浏览这个被“下毒”的页面时,嵌入的恶意代码会被浏览器自动执行,黑客就能趁机达成恶意目的。
这里要特别说明下,早期 XSS 也被简写为 CSS,但为了和我们常用的“层叠样式表(CSS)”区分开,现在更规范的叫法是 XSS。它的攻击场景特别普遍,比如评论区、留言板、用户资料编辑页等任何允许用户输入内容的地方,都可能成为 XSS 的突破口。
安全级别:
高风险!前端页面的“隐形炸弹”,用户无感知中招,影响范围极广。
2. 核心风险:会话被盗,身份被冒充
XSS 的危害远比想象中严重,黑客通过恶意代码能实现这些操作:
- 窃取用户的会话 Cookie 和登录态,直接冒充用户身份登录系统;
- 篡改页面内容,比如伪造虚假公告、钓鱼链接,诱导用户输入更多敏感信息;
- 劫持用户浏览器,强制跳转到恶意网站,或弹出垃圾广告、诈骗窗口;
- 更严重的情况下,可通过恶意脚本获取用户设备信息,甚至控制设备。
3. 解决方案:从用户到开发者的双重防护
XSS 的防御需要“用户端+开发端”配合,双管齐下才能筑牢防线:
- 用户端防护:在浏览器设置中禁用 JavaScript 脚本(注:此方案会影响部分网站正常功能,适合对安全性要求极高的场景);
- 开发端防护(核心):仔细审核所有用户输入相关代码,对提交的输入数据进行严格检查和过滤——比如过滤 HTML 标签、转义特殊字符(如<、>、’、”等),避免用户输入的内容被当作代码执行。
4. 技术实现:复用过滤器,低成本落地防护
XSS 的防护可直接复用我们在“SQL 注入”漏洞中提到的过滤器技术(具体实现参见 PART 3 下的”技术实现”),核心逻辑完全一致:
通过自定义过滤器对所有用户输入的参数进行扫描,一旦检测到<script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js"></script><script type="6f0c4e94a628ed0c681e51ac-text/javascript">、onclick、onload 等恶意代码相关字符,直接弹出错误提示并阻断请求,从源头阻止恶意代码注入。这种方案无需大幅修改业务代码,侵入性小,适合已上线系统和开发中系统通用。
跨站点请求伪造(CSRF)—— 被盗的“身份凭证”
中风险漏洞
1. 漏洞描述:冒充你发请求,钱没了都可能不知情
CSRF(全称Cross-site request forgery,跨站请求伪造),通俗理解就是“黑客盗用你的身份,以你的名义发送恶意请求”。它还有个更形象的名字——“一键攻击”,因为很多时候用户只需点击一个链接、打开一封邮件,就会在不知情的情况下触发攻击。
举个直观的例子:你刚登录银行网站完成转账,没退出登录就点开了一封垃圾邮件。邮件里藏着一个隐藏的转账请求链接,浏览器会自动携带你银行网站的登录 Cookie 发起请求,银行验证 Cookie 合法后,就会执行这笔“被伪造”的转账操作,而你全程毫不知情。
安全级别:
中风险!依赖用户已登录的前提,但攻击成功后可能造成财产损失和隐私泄露。
2. 核心风险:隐私泄露+财产安全危机
CSRF 能做的坏事远超你的想象,包括但不限于:
- 以你的名义发送邮件、发布不良言论,败坏你的声誉;
- 盗用你的社交账号、购物账号,发送垃圾信息或恶意下单;
- 针对金融类应用,发起转账、支付等操作,直接造成你的财产损失;
- 修改你的用户资料(如密码、绑定手机号),彻底掌控你的账号。
3. 解决方案:给请求加“验证码”,拒绝非法请求
CSRF 的核心漏洞是“服务器仅通过 Cookie 验证身份,无法区分请求是用户主动发起还是黑客伪造”,因此防御的关键是给合法请求加“唯一标识”:
- 严格区分请求类型:仅允许 GET 请求用于检索数据(如查看订单),绝不允许用 GET 请求执行修改数据、转账、删除等核心操作——这能直接防止黑客通过
标签等方式触发GET型恶意请求;
- 给 POST 请求加“随机 Token”:要求所有涉及数据修改的POST请求,都必须携带一个服务器生成的随机 Token(相当于请求的“验证码”),服务器仅接收 Token 合法的请求。
4. 技术实现:Token+过滤器,全方位拦截伪造请求
CSRF 防护的核心是“Token 验证机制”,具体实现分为 4 个步骤,含完整代码可直接复用:
(1)核心逻辑说明
在用户登录时,服务器生成一个随机且唯一的 Token,存储在 Session 中;前端页面发起请求时,需携带这个 Token(POST 通过隐藏域、GET 通过 URL 参数);服务器通过过滤器拦截所有请求,验证请求携带的 Token 与 Session 中的 Token 是否一致,一致则放行,不一致则阻断。
(2)具体落地步骤
- 添加 Token 隐藏域 /URL 参数:
- POST 请求页面:添加固定名称的隐藏域,值从 Session 中获取
<input name="CSRF_TOKEN" type="hidden" id="CSRF_TOKEN" value="<%=session.getAttribute("CSRF_TOKEN") %>"/>
- GET 请求页面:在 URL 后拼接 Token 参
window.location='loginProcess.jsp?CSRF_TOKEN=<%=session.getAttribute("CSRF_TOKEN") %>'
*2. 编写 CSRF 过滤器(核心代码)*:
public class CsrfFilter implements Filter { private String notProtect; // 不受保护的资源集合(如登录页) private String tokenTimeout; // token 过期时间(默认10分钟) public void init(FilterConfig filterConfig) throws ServletException { notProtect = filterConfig.getInitParameter("notProtect"); tokenTimeout = filterConfig.getInitParameter("TokenTimeOut"); if (null == tokenTimeout) { tokenTimeout = "10"; } } public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpSession session = req.getSession(true); // 排除无需保护的资源 if (!FilterUtils.isContainUrl(notProtect, FilterUtils.getFullUrlFromRequest(req))) { String requestToken = req.getParameter(TokenBuilder.CSRF_NONCE_REQUEST_PARAM); Object sessionTokenObj = session.getAttribute(TokenBuilder.CSRF_NONCE_REQUEST_PARAM) ; String sessionToken = sessionTokenObj == null ?"":(String)sessionTokenObj; // 若Session中无Token,生成新Token并存储 if (sessionToken.equals("")) { sessionToken = TokenBuilder.generateToken(); session.setAttribute(TokenBuilder.CSRF_NONCE_REQUEST_PARAM, sessionToken); } // 验证请求Token的有效性 if (null != requestToken) { // 检查Token是否过期 if(TokenBuilder.checkTokenTimeout(requestToken, tokenTimeout) ){ sessionToken = TokenBuilder.generateToken(); session.setAttribute(TokenBuilder.CSRF_NONCE_REQUEST_PARAM, sessionToken); } // 验证Token是否匹配,不匹配则阻断 if(!TokenBuilder.isTokenValid(sessionToken,requestToken)){ response.setContentType("text/html; charset=GBK"); response.getWriter().write("<script language='javascript'>alert('无效的安全标识符,请重新刷新当前页面!');history.go(-1);</script>"); return; } } } filterChain.doFilter(request, response); } public void destroy() {}}
- 编写Token生成与验证工具类(TokenBuilder.java):
public class TokenBuilder { private static Map map = new HashMap(); public static final String CSRF_NONCE_REQUEST_PARAM = "CSRF_TOKEN"; // 生成随机Token(含时间戳,确保唯一性) public synchronized static String generateToken(){ byte random[] = new byte[16]; StringBuffer buffer = new StringBuffer(); SecureRandom randomSource = new SecureRandom(); randomSource.nextBytes(random); for (int j = 0; j< random.length; j++) { byte b1 = (byte) ((random[j] & 0xf0) >> 4); byte b2 = (byte) (random[j] & 0x0f); if (b1 < 10) buffer.append((char) ('0' + b1)); else buffer.append((char) ('A' + (b1 - 10))); if (b2 < 10) buffer.append((char) ('0' + b2)); else buffer.append((char) ('A' + (b2 - 10))); } long nowTime = System.currentTimeMillis(); String token = nowTime +":"+ buffer.toString(); map.put(token, null); return token; } // 检查Token是否过期 public synchronized static boolean checkTokenTimeout(String token,String tokenTimeout) { boolean isTimeout = false; long term = Long.parseLong(tokenTimeout) * 60 * 1000; // 转换为毫秒 Iterator it = map.keySet().iterator(); long nowTime = System.currentTimeMillis(); try{ long tokenTime = Long.valueOf(token.split(":")[0]).longValue(); if((nowTime - tokenTime) >= term){ isTimeout = true; } }catch(Exception e){ isTimeout = true; } // 清理过期Token,避免内存溢出 while (it.hasNext()) { String key = (String) it.next(); long oldTokenTime = Long.valueOf(key.split(":")[0]).longValue(); if ((nowTime - oldTokenTime) >= term) { it.remove(); } } return isTimeout; } // 验证Token是否有效(匹配+存在) public synchronized static boolean isTokenValid(String savedToken,String token) { boolean isTokenValid = map.containsKey(token); if(savedToken.equals(token)){ if (isTokenValid) { map.remove(token); // 一次性Token,使用后删除 } isTokenValid = true; }else{ if (isTokenValid) { map.remove(token); } } return isTokenValid; }}
- 配置 web.xml(注册过滤器):
<filter> <filter-name>CsrfFilter</filter-name> <filter-class> cn.com.victorysoft.devcenter.component.usrmgr.security.filter.CsrfFilter </filter-class> <init-param> <param-name>TokenTimeOut</param-name> <param-value>15</param-value> <!-- Token过期时间:15分钟 --> </init-param></filter><filter-mapping> <filter-name>CsrfFilter</filter-name> <url-pattern>*.jsp</url-pattern> <!-- 拦截所有JSP请求 --></filter-mapping>
重要说明:
FilterUtil 类代码可直接复用 PART 3 下“SQL 注入”中的辅助类代码,无需重复编写;
此方案是目前防御 CSRF 的唯一可行方案,但会增加一定开发工作量,且对系统效率有轻微影响。是否落地需结合业务安全性需求综合评估——金融、电商等涉及资金和核心数据的系统建议优先部署。
连
载
预
告
安全攻击及防范手册
今天我们搞定了前端交互层的两大漏洞——XSS 和 CSRF。
总结一下核心要点:XSS 是“给页面下毒”,防御关键是过滤用户输入;CSRF 是“盗用身份发请求”,防御核心是给请求加 Token 验证。这两种漏洞都瞄准了用户会话和身份,前端同学一定要重点关注!
下一篇(PART 5),我们将把视线拉回后端,拆解两个容易被忽视的漏洞:发现数据库错误模式和应用程序错误。很多时候,黑客就是通过系统返回的错误提示,摸清了数据库结构和代码逻辑,进而发起精准攻击。这部分内容关乎后端安全的“底线”,千万别错过!
你在项目中有没有遇到过 XSS 或 CSRF 攻击?或者在部署 Token 验证时遇到过难题?欢迎在评论区留言讨论,我们将在后续文章中针对性解答!
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:野猪与安全 耶度《前端藏刀!XSS 与 CSRF 的攻防博弈——Web 应用安全实战(PART 4)》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论