XSS(跨站脚本攻击)长期占据 Web 安全威胁榜单的前列。它的核心逻辑并不复杂:攻击者设法让恶意脚本在受害者的浏览器中执行,从而窃取 Cookie、会话令牌,或冒充用户发起操作。对于前端开发者来说,理解 XSS 的常见注入路径,并在编码阶段就建立起防御体系,远比事后补救更有效。
XSS 的三种常见方式
存储型 XSS 是最危险的一类。攻击者将恶意脚本提交到服务器并持久化保存,比如在评论区、用户昵称或商品评价中植入 <script> 标签。当其他用户浏览这些内容时,脚本自动执行。这类攻击影响范围广,因为每个访问该页面的用户都会中招。
反射型 XSS 则依赖诱导点击。攻击者构造一个带有恶意参数的 URL,诱使受害者点击。服务器将参数原样“反射”回页面,浏览器将其当作脚本执行。常见于搜索框、错误提示页等场景。
DOM 型 XSS 完全发生在浏览器端。前端 JavaScript 代码从 location.hash、document.referrer 等来源读取数据,并直接写入 innerHTML、eval() 或 document.write() 中。服务器甚至不知道攻击发生,因为恶意载荷从未到达后端。
这三种方式的共同点是:不可信数据进入了执行上下文。防御的核心思路就是切断这条路径。
前端防御的三层策略
第一层:输入过滤——不信任任何用户输入
输入过滤的目标是在数据进入系统时就剔除或转义危险内容。但需要明确一点:输入过滤不能替代输出编码。它更像一道前置门槛,用于拦截明显恶意的载荷。
在实践中,可以采用以下做法:
- 对表单字段实施白名单校验。例如用户名只允许字母、数字和下划线,使用正则表达式
^[a-zA-Z0-9_]{3,20}$进行约束。 - 对富文本内容,使用成熟的净化库如 DOMPurify,而非自己编写正则。DOMPurify 会解析 HTML 并移除所有危险标签和属性。
- 对 URL 参数进行类型检查和长度限制,避免超长载荷绕过检测。
import DOMPurify from 'dompurify';
const dirty = '<img src=x onerror=alert(1)>';
const clean = DOMPurify.sanitize(dirty);
// 输出: <img src="x">
需要注意的是,输入过滤应作为纵深防御的一环,而不是唯一依赖。攻击者可以通过编码变形、大小写混合等方式绕过简单的黑名单。
第二层:输出编码——根据上下文选择编码方式
输出编码是防御 XSS 最有效的手段。原则很简单:在将数据插入 HTML 文档之前,根据插入位置进行对应的编码。
- HTML 正文中:将
&、<、>、"、'分别编码为&、<、>、"、'。 - HTML 属性中:除了上述字符,还需对空格和反引号进行编码,并始终使用引号包裹属性值。
- JavaScript 上下文中:使用
JSON.stringify()序列化数据,避免直接拼接字符串。 - URL 参数中:使用
encodeURIComponent()进行编码。
现代前端框架(React、Vue、Angular)默认会对插值内容进行转义。但危险往往出现在开发者主动绕过这些保护时:
// 危险:直接使用 innerHTML
element.innerHTML = userInput;
// 安全:使用 textContent
element.textContent = userInput;
// React 中危险:dangerouslySetInnerHTML
<div dangerouslySetInnerHTML={{__html: userInput}} />
// 如必须使用,先经过 DOMPurify
<div dangerouslySetInnerHTML={{__html: DOMPurify.sanitize(userInput)}} />
第三层:CSP——最后一道防线
内容安全策略(Content Security Policy)通过 HTTP 响应头告诉浏览器:只允许加载哪些来源的脚本、样式和资源。即使攻击者成功注入了脚本,CSP 也能阻止其执行。
一个实用的 CSP 配置示例:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://trusted.cdn.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
关键点说明:
script-src 'self'禁止执行内联脚本和外部不可信脚本,这是阻止 XSS 最有效的一条。- 避免使用
'unsafe-inline'和'unsafe-eval',它们会大幅削弱 CSP 的防护效果。 object-src 'none'禁止加载 Flash 等插件内容。frame-ancestors 'none'防止页面被嵌入 iframe,抵御点击劫持。
对于必须使用内联脚本的场景,可以使用 nonce 机制:服务器每次响应生成一个随机数,只有携带该 nonce 的 <script> 标签才会被执行。
<script nonce="random-value-here">
// 合法内联脚本
</script>
关联话题:SQL 注入与服务器加固
XSS 和 SQL 注入常常被相提并论,因为它们都源于“将不可信数据当作代码执行”。在 MySQL 中,防止 SQL 注入的核心写法是使用参数化查询:
// 危险:字符串拼接
$sql = "SELECT * FROM users WHERE name = '$name'";
// 安全:预处理语句
$stmt = $pdo->prepare("SELECT * FROM users WHERE name = ?");
$stmt->execute([$name]);
无论使用 PDO、MySQLi 还是 ORM 框架,参数化查询都应成为默认选择。存储过程、白名单校验和最小权限原则可以作为补充,但不能替代参数化。
在 Linux 服务器层面,安全加固清单包括:及时更新系统和软件补丁、禁用 root 远程登录、使用 SSH 密钥认证、配置防火墙只开放必要端口、启用 fail2ban 防止暴力破解、定期审计日志和权限。这些措施与前端 XSS 防御一样,都是纵深防御体系的组成部分。
总结
XSS 防御不是单一技术能解决的问题。输入过滤拦截明显恶意内容,输出编码根据上下文正确转义数据,CSP 作为最后一道防线限制脚本执行来源。三者叠加,才能形成有效的防护。前端开发者应当养成习惯:永远不信任用户输入,永远根据输出上下文选择编码方式,永远为生产环境配置合理的 CSP 头。安全不是一次性的任务,而是贯穿开发流程的持续实践。
未经允许不得转载:任鹏个人博客 » 前端防御 XSS:输入过滤、输出编码与 CSP 落地


朋友圈点赞图在线生成源码