7.1 XSS注入与会话固定的复现封堵


7.1 XSS注入与会话固定的复现封堵

本节摘要:经典漏洞的防护不能靠背名单,要靠复现——亲手打穿一次,胜过读十篇科普。本站在青梧书肆的本地环境里逐一复现脚本注入、查询注入、跨站伪造与会话固定,再逐一封堵:输出转义、参数化查询、令牌校验、登录重建会话。每个漏洞对应信任边界上的一道闸门,复现教会你它为什么能进来,封堵教会你在哪关门。

复现一次留言板攻击

老站几乎一定有一个留言或评论入口,青梧书肆的图书评论页就是教科书级标本。先复现:用测试账号发一条评论,内容里夹一段脚本:

这本《夜航西飞》太好了 <script>new Image().src = '/collect?c=' + document.cookie;</script>

提交成功。换另一个账号打开这本书的评论页,再看攻击者那边的收集端——收到了一条带 Cookie 的请求。攻击成立,而且是最阴险的存储型:脚本随评论存进数据库,之后每个打开该页的用户都会中招,攻击一次、收割长期。

拆解它为什么能成,会发现两个环节同时失守。其一是出门没洗:评论内容原样拼进 HTML,浏览器分不清哪段是内容、哪段是指令,一律执行。封堵动作是输出转义——所有动态内容出站前把特殊字符转成实体:

<%-- 改造前:原样输出,等于帮攻击者执行脚本 --%> <p>${requestScope.comment.body}</p> <%-- 改造后:转义输出,脚本变成无害文本 --%> <p><c:out value="${requestScope.comment.body}"/></p>

其二,就算某个页面忘了洗,损失也可以被限制:会话凭证 cookie 加上只读标志与同站标志后,脚本读不到、跨站带不走。输出转义与凭证保护是两层互补的网,别以为配了第二层就可以省第一层。

图7-1 攻击面与四道闸门

图7-1 攻击面与四道闸门

查询注入:拼接的代价

如果说脚本注入打的是浏览器,查询注入打的就是数据库。老代码里的登录查询多半长这样:

// 拼接版:attack 输入 admin' -- 即可绕过密码 String sql = "select * from member where account='" + account + "' and password='" + password + "'";

账号栏输入 admin' --,密码随便填,拼出来的查询里密码条件被注释符吞掉——密码校验形同虚设。修复方案 4.3 节其实已经用上了:预编译语句,结构归结构、数据归数据:

PreparedStatement ps = conn.prepareStatement( "select * from member where account=? and password=?"); ps.setString(1, account); ps.setString(2, password);

参数无论塞进什么字符,都只当数据解释,永远变不成指令。全站自查动作很机械:搜索所有手写查询,逐个确认参数化;确需动态拼接表名、排序字段的场景(预编译管不了结构),改用白名单校验——允许的取值列成表,不在表内的直接拒绝。

会话的两场暗算:伪造与固定

第三类攻击不偷数据、不破指令,它借你的身份办事。跨站伪造的剧本:你登录着书店,又顺手打开一个恶意页面,里面一个隐藏表单自动向书店的下单接口发请求——浏览器发请求时自动带上你的会话凭证,书店一看凭证有效,订单照做。你的浏览器成了攻击者的傀儡。

封堵的思路是打破"有凭证即本人意愿"的假设:给写操作表单加一个攻击者猜不到、也拿不到的令牌。会话里存一份,表单里藏一份,服务端比对:

// 登录成功时 String token = UUID.randomUUID().toString(); session.setAttribute("csrfToken", token);
<form method="post" action="${pageContext.request.contextPath}/order"> <input type="hidden" name="token" value="${sessionScope.csrfToken}"/> <%-- 其余表单字段 --%> </form>
// 下单控制器入口 if (!Objects.equals(request.getParameter("token"), session.getAttribute("csrfToken"))) { resp.sendError(403); return; }

恶意页面能诱导浏览器发请求,却读不到你会话里的令牌值,表单里那份它填不出来,比对必然失败。

第四类暗算更冷门但老站高发:会话固定。攻击流程是先从站点领一个会话编号,诱骗受害者用这个编号登录(伪造的链接里带着编号),受害者登录成功后,编号背后的身份升级成了受害者——攻击者拿着同一个编号直接登门。防御动作只有一句:登录成功后废弃旧会话、重建新会话,旧的编号随失效的会话一起作废:

// 登录成功后、写入身份之前 request.getSession().invalidate(); // 废弃可能被预设的旧会话 HttpSession fresh = request.getSession(true); // 重建全新会话 fresh.setAttribute("user", loginUser);

案例:一份渗透报告的三条整改

背景:上线前的外部渗透测试交来报告,青梧书肆中三条:评论页存在存储型脚本注入;登录接口存在会话固定;下单接口缺少跨站伪造防护。三条都是本节讲过的病,正好整编成一份整改实录。

操作:按"先止损、再根治"排序。脚本注入最急——全站动态输出点逐一过 c:out,顺手把凭证标志配齐;会话固定改登录流程,重建会话两行代码;伪造防护铺令牌,写操作控制器统一在入口比对。整改完在本地把三个攻击手法各复现一遍,确认全部失效。

结果:复测三项全绿。顺带的收获是全站输出点清单——后来成了交接文档里"动态输出登记表"的底稿。

解读:这次整改最有价值的产出不是三个修复,而是那个副产品:输入输出点清单。安全维护最怕的不是漏洞多,而是攻击面说不清——有了清单,后续每次审计从扫描变成核对,成本降一个量级。接手任何老站,先画攻击面清单,再谈修复,这个顺序别反。

变式:如果站点暂时无法全面整改(改动窗口受限),优先级这样排:查询注入必须立即封(直接资损);脚本注入至少先加凭证保护止损;伪造防护可短期靠关键操作二次确认顶住。风险排序的依据是资损速度,不是漏洞的知名度。

⚠️ 常见坑:只在页面入口做校验、就当进了保险箱——绕过入口的路径多得是(直接调接口、参数改包)。校验和转义必须落在"数据真正被使用"的那一层:查询前参数化,输出前转义,谁使用谁设防。

本节要点回顾

  • 先复现再封堵:每个漏洞亲手打穿一次,攻击原理与封堵位置同时刻进肌肉记忆;
  • 出门必洗:动态输出一律转义,c:out 是视图层的默认动作而非可选项;
  • 结构数据分离:预编译语句根治查询注入,动态结构用白名单,拼接查询是全站搜索即得的事故名单;
  • 令牌破伪造:会话存、表单带、服务端比,三件套缺一不可;
  • 登录重建会话:废弃旧会话再写入身份,预设编号随旧会话一同作废。

攻击面封完了,还有一路"不请自来"的访客:错误。下一站把错误页、异常映射与线上排障的路数补齐,安全课就完整了。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U