是真的小外套头像
网络安全实战第23期:存储型与DOM型XSS的本质区别封面图

网络安全实战第23期:存储型与DOM型XSS的本质区别

存储型与 DOM 型 XSS:一个内容进了数据库,一个服务器从没看见

网络安全实战 90 天 · 第 23 篇 | Week 4 客户端与服务端两条线 | 入门 | 约 2.5 小时

上一篇的反射型有个明显限制:必须骗人点那条链接。今天补上另外两种——存储型把内容写进数据库,谁来看谁中招;DOM 型则连服务器都没参与,前端脚本自己把 URL 里的内容拼进了页面。三种类型的脚本长一个样,处理方式却完全不同,本篇把它们放在一起对比。

一、实验环境与本次目标

项目内容
靶场本地搭建的测试靶场
测试模块XSS (Stored) 与 XSS (DOM)
安全级别Low
本次目标分别确认两种类型的触发条件与差异
边界仅在本机靶场验证,不做任何数据外带

两种类型都用同一个验证动作:让页面上出现一个可见的标记。区别在于「这个标记是怎么留下来的」。

二、存储型:内容一旦进去,就留在了那里

该靶场的存储型模块是一个留言板,名字和内容两个输入框,提交后会一直显示在页面上,谁来访都能看到。先正常提交一条:

先正常留一条

Name:Hacker
Message:这是一条普通留言

提交后,页面上多出一行留言,刷新还在,换浏览器打开还在。这说明内容被写进了数据库,而不是只停留在这一次请求的响应里。

接着把内容换成一段脚本再提交一次:

把留言内容换成一个标签

Name:Hacker
Message:<script>alert('XSS')</script>

提交的瞬间页面就弹窗了——它甚至不需要我再刷新,因为服务端把刚落库的内容立刻渲染回来了。更值得注意的是接下来的三步验证:

  • 刷新页面,依旧弹窗——内容还在数据库里。

  • 换一个浏览器打开同一个地址,依旧弹窗——这不是缓存在作怪。

  • 清掉缓存重进,依旧弹窗——结论只能是「脚本已经在数据库里,谁看谁执行」。

存储型和反射型的本质差别 反射型是「我点它才响」,存储型是「它一直在这儿响」。前者需要一次诱导点击,后者只需要让一个人打开页面。利用成本从「说服一个人点链接」降到「等人来访问」,这就是它危害更高的原因。

三、存储型更危险的三个原因

同为弹窗,存储型在真实场景里的影响要大得多,理由只有三条,但每条都很硬。

原因说明
无需投递内容已经在站点里,不用发邮件、不用发短链,等访问自然发生
影响所有访客同一页面被多少人看,就有多少次执行,包括管理员
持久存在不清理就会一直在,修复前每一次访问都触发一次

第二行在实战中最要命:管理员往往有一个「审核留言」的后台页面,如果这个后台把留言内容直接渲染出来,那么一条留言就能让管理员的会话被影响。这也是为什么表单、评论、昵称、签名这些「用户可写、别人会看」的字段,一直是这类问题的重灾区。

四、DOM 型:服务器完全没有参与

进入 DOM 型模块,先正常访问:

正常访问

http://[靶场地址]:8082/vulnerabilities/xss_d/?default=English

页面会显示一个下拉框,默认选中的是 English。把参数换掉再访问:

把参数换成标签

http://[靶场地址]:8082/vulnerabilities/xss_d/?default=<script>alert(1)</script>

页面同样弹窗了。但如果这时候去看服务器返回的 HTML 源码,会发现里面根本没有这段脚本——服务器返回的页面是干净的。

原因在前端脚本里:页面加载后,JavaScript 从地址栏读取 default 参数,然后把它写进某个元素的内容里。整个过程里,服务器只是一个「送快递的」,它送来的包裹是正常的,是收件人自己在家里把包裹拆开点燃了。

DOM 型的判定特征 服务器响应里看不到脚本,但页面执行了;脚本从 URL 或页面属性进入,由前端脚本拼接进 DOM。这决定了防护位置:服务端过滤对它无效,必须在拼接的那一行做处理。

五、三种类型的七个维度对照

把这三种放在一起比一遍,比单独记每一种都清楚。这张表是我自己整理后反复回看的一张。

维度反射型存储型DOM 型
脚本位置URL 参数数据库URL 或页面属性
持久性一次性持久一次性
触发方式诱导点击打开页面诱导点击
影响范围点的那个人所有访客点的那个人
服务器是否参与参与(回显)参与(存取)不参与
后端日志常见 payload常见 payload通常无痕迹
内容过滤能否拦截能能不能

最后一行是我认为最有价值的一行。前两种类型里,服务端确实能看到那段输入,有机会拦;DOM 型里服务端什么都没看见,任何服务端过滤都形同虚设。

记法:反射型靠诱导,存储型靠等待,DOM 型靠前端。判断类型时,先问「这段内容服务器看见了吗」,再问「它留在哪儿、活多久」。

六、前端最危险的几处拼接

DOM 型的问题最终都落在一行代码上:把不可信的内容拼进页面。常见的几类写法如下,做代码自查时优先搜这几个名字。

写法风险来自哪里
innerHTML / outerHTML把字符串当 HTML 解析,标签会被执行
document.write直接向文档流写入,等同解析 HTML
eval / new Function把字符串当脚本运行,最直接的一类
location.href / location.replace跳转到可控地址,常用于跳转到伪装页
setTimeout / setInterval 的字符串参数字符串形式会被当脚本执行
v-html、dangerouslySetInnerHTML 之类的框架语法框架里同样存在,只是换了个名字

对应的正确写法,核心只有一句:把它当文本,而不是当 HTML。设文本用 textContent 或框架的插值,确实需要富文本时走白名单清洗,而不是自己写替换规则。

七、自查清单:三类一起查

  • 把所有「用户写、别人看」的字段列出来:留言、评论、昵称、签名、工单描述、文件名。这些是存储型的首选位置。

  • 在每个字段里输入一个特殊字符,提交后查看页面源码,确认是实体编码还是原样输出。

  • 对每个页面做一次「换浏览器是否复现」的验证,这一步能直接区分反射型和存储型。

  • 在前端代码里搜上面那张表的六个名字,逐个确认拼进去的内容是否可信。

  • 检查 Cookie 的 HttpOnly 标记与响应头的 CSP,这两项决定「即使执行了,能拿走多少」。

经验值:存储型的排查成本远低于它的危害。一个字段一个字段试一遍,通常十几分钟就能把重灾区找出来。

八、踩坑记录

现象原因处理
留言内容太短提交不上字段长度限制只留最精简的验证标记,或改用事件属性写法
输入被过滤掉了服务端做了黑名单换一种事件触发写法验证是否真被拦住
DOM 模块没反应浏览器对某些写法的处理不同换浏览器复测,或用页面上的可见变化作为信号
以为是存储型其实是反射型没有做换浏览器/清缓存的对照固定做三步验证:刷新、换浏览器、清缓存

九、小结

  • 存储型的关键动作是「换浏览器仍复现」,这一步排除了缓存干扰,证明内容已落库。

  • DOM 型的关键证据是「服务器响应里没有脚本,页面却执行了」。

  • 服务端过滤只对前两种有效,DOM 型只能在前端拼接处解决。

  • 同一个弹窗,三种类型的处理位置完全不同,判断类型比复现更重要。

下一篇是这一周的转折点:当过滤开始出现,为什么大多数过滤都能被绕过去,而白名单又为什么难绕。我们不讲具体写法,讲的是「过滤判断依据在哪一层」。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/2401_88349202/article/details/170129621

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--