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, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } /* 场景:必须用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上下文仍可被绕过 |
白名单优先 | 优先白名单,黑名单只做辅助,不做核心防御 | 写一堆过滤 |
/* 输入校验:白名单示例,只允许字母数字、下划线 */ 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)做不同封装处理;原料不能只靠进厂检查,送到不同容器时都要重新适配防护。