XSS 攻击与防御

10.1 XSS 攻击与防御

XSS 的本质可以类比为在网页里混入一段不受信任的剧本,浏览器无法区分脚本是业务本身的代码还是攻击者注入的内容,会直接解析执行,窃取数据、劫持页面、诱导跳转。

10.1.1 XSS 分类:反射型、存储型、DOM 型

XSS(跨站脚本攻击)是攻击者向页面注入恶意脚本,在用户浏览器中执行,以此窃取会话、篡改页面、发起请求的一类前端注入攻击。

XSS 类型

触发链路

数据来源

风险持久度

典型识别特征

反射型 XSS

恶意参数随URL传给后端,后端直接拼入页面返回

URL 查询参数

一次性,链接失效即失效

需要诱导用户点开特制链接

存储型 XSS

恶意内容提交到服务器持久存储,其他用户访问页面时加载渲染

评论、留言、帖子等入库内容

持久存在,所有访问者都会中招

危害最大,属于蠕虫式风险

DOM型 XSS

完全在前端JS内处理,不经过后端返回恶意HTML,前端脚本读取URL、hash等数据直接操作DOM

location.hash、search、localStorage等前端变量

取决于页面逻辑,后端不感知攻击

抓包看响应体无恶意代码

<!-- 不安全示例:直接拼接用户输入到DOM,DOM型XSS漏洞 --> <div id="tip"></div> <script> const userInput = new URLSearchParams(location.search).get('msg'); /* 漏洞:innerHTML会解析标签与脚本,输入 <script>alert('xss')</script> 就会执行 */ // document.getElementById('tip').innerHTML = userInput; /* ✅ 安全写法1:使用textContent,只会当做纯文本渲染,不会解析HTML */ document.getElementById('tip').textContent = userInput; </script> // 防御辅助:简易HTML转义函数(输出编码) function htmlEncode(str) { if (!str) return ''; return str .replace(/&/g, '&amp;') .replace(/</g, '&lt;') .replace(/>/g, '&gt;') .replace(/"/g, '&quot;') .replace(/'/g, '&#39;'); } /* 场景:必须用innerHTML渲染内容时,先转义再写入 */ // elem.innerHTML = htmlEncode(userInput);

浏览器渲染页面就像放映员放映影片,XSS就是攻击者偷偷塞进去的盗版胶片;

反射型是一次性的盗版碟,点开才播放;存储型是把盗版碟放进影院片库,谁来看都播放;

DOM型是放映机自己拿到外部信息,自行拼接出盗版画面。


10.1.2 防御核心手段

XSS 防御核心手段是一套分层防护策略,从输入校验、输出编码、安全头、沙箱隔离多维度阻止恶意脚本被浏览器解析执行。

防御手段

作用说明

适用范围

注意事项

输出编码(转义)

根据输出上下文转义特殊字符,破坏脚本语法

HTML、JS、CSS、URL 上下文

不同上下文转义规则不一样,不能通用

CSP内容安全策略

限制页面允许加载、执行的脚本来源,阻断内联脚本、eval

全局防护,优先推荐

配置不当容易误杀正常业务脚本

HttpOnly Cookie

禁止JS读取Cookie,就算脚本执行也拿不到会话凭证

Cookie安全防护

无法防御页面篡改、钓鱼类XSS危害

安全DOM API

优先 textContent、setAttribute,避免 innerHTML、document.write

前端DOM操作

不能作为唯一防御,仅减少漏洞入口

输入白名单校验

只放行合法格式、字符,拒绝不在白名单内的数据

表单、提交内容

白名单优于黑名单,黑名单容易被绕过

<!-- CSP 示例:meta标签方式设置(仅作演示,优先后端响应头设置) --> <meta http-equiv="Content-Security-Policy" content=" default-src 'self'; script-src 'self' [https://trusted-cdn.com](https://trusted-cdn.com); style-src 'self'; img-src 'self' data:; object-src 'none'; "> <!-- 含义:只允许本站和可信CDN的脚本,禁止内联脚本、eval、插件 --> // 高危API规避示例 const box = document.querySelector('.box'); /* ❌ 危险:document.write 会解析HTML,极易引入XSS */ // document.write(userContent); /* ✅ 安全方式:纯文本写入 */ box.textContent = userContent; /* 如需设置属性,使用setAttribute,不要直接拼接字符串到html */ box.setAttribute('title', userContent);

CSP相当于影院的准入安检,只允许指定供应商的影片进场;

HttpOnly相当于把重要的会员卡锁进保险柜,放映员(JS)没有钥匙;

输出编码就是把可疑胶片上的特殊标记涂抹掉,让放映机识别不出这是可执行剧本。


10.1.3 输入输出过滤原则

输入输出过滤原则是区分信任边界,对不可信数据在输入、输出两个环节做校验与编码,避免原始可控数据流入解析上下文。

原则

核心含义

常见误区

不信任原则

所有外部输入默认不可信,包括URL、表单、后端返回、本地存储

认为后端校验过,前端就可以直接使用

输出优先原则

编码放在输出渲染阶段,而不是只在输入时一次性过滤

输入转义后存入数据库,多次取出时重复转义造成乱码

上下文匹配原则

在哪种上下文渲染,就用对应规则编码(HTML/JS/CSS/URL)

一套转义函数到处复用,JS上下文仍可被绕过

白名单优先

优先白名单,黑名单只做辅助,不做核心防御

写一堆过滤<script>的黑名单,容易被注释、大小写绕过

/* 输入校验:白名单示例,只允许字母数字、下划线 */ function validateUsername(input) { const reg = /^[a-zA-Z0-9_]{1,20}$/; /* 不符合白名单直接拒绝,不进入后续逻辑 */ return reg.test(input); } /* 输出编码:不同上下文分开处理 */ function renderHtmlContext(unsafeStr) { return htmlEncode(unsafeStr); } function renderJsContext(unsafeStr) { /* JS上下文需要额外转义 \ " ' 等,不要复用HTML转义函数 */ return JSON.stringify(unsafeStr); }

输入输出过滤就像食品加工厂的质检:输入是原料进厂检查,过滤明显异物;输出是包装分装,根据包装容器(HTML/JS/CSS)做不同封装处理;原料不能只靠进厂检查,送到不同容器时都要重新适配防护。