安全编码实战:避开常见漏洞的实用开发技巧
前端安全编码,我到底该用哪个方案?
最近在重构一个老项目,突然被安全团队扫出一堆 XSS 漏洞,主要是用户输入内容没做转义就直接插进 DOM。这事儿说大不大,说小不小,但真要线上出事,背锅的还是我们前端。于是我就花了一天时间,把主流的几种前端安全编码方案都试了一遍,踩了不少坑,也有了点自己的偏好。今天就来聊聊这几个方案的实际体验。
谁更灵活?谁更省事?
我主要对比了三种方案:手动写 escapeHTML 函数、用 DOMPurify 库、以及用 React/Vue 的内置机制(比如 JSX 自动转义)。别看都是“防 XSS”,实际用起来差别挺大。
先说最原始的——自己写个 escapeHTML 函数。这招我在早期项目里常用,代码简单,控制感强:
function escapeHTML(str) {
if (typeof str !== 'string') return str;
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
用起来就是:
const userInput = '<script>alert("xss")</script>';
document.getElementById('output').innerHTML = escapeHTML(userInput);
优点是轻量、无依赖,适合那种“不能加任何第三方库”的老系统。但问题也很明显:**你只处理了 HTML 特殊字符,没处理上下文**。比如,如果这段内容被插进 <script> 标签里,或者作为 onclick 的属性值,光转义 < 是没用的。我之前就在这栽过跟头——以为转义了就安全,结果攻击者用 javascript: 伪协议绕过了。所以,除非你 100% 确定输出位置是纯文本上下文,否则别轻易用这个方案。
DOMPurify:我的首选,但不是万能药
现在我基本都用 DOMPurify。它不只是转义,而是对整个 HTML 字符串做“净化”(sanitize),把危险标签和属性干掉,保留安全的结构。比如用户输入一段带样式的评论,你想保留 <b>、<i>,但去掉 <script> 和 onerror,DOMPurify 就特别合适。
import DOMPurify from 'dompurify';
const dirty = '<b>Hello</b><img src=x onerror=alert(1)>';
const clean = DOMPurify.sanitize(dirty);
document.getElementById('output').innerHTML = clean;
// 输出:<b>Hello</b><img src="x">
我比较喜欢它的配置灵活性。比如你可以自定义允许的标签:
const config = {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong'],
ALLOWED_ATTR: ['class']
};
const clean = DOMPurify.sanitize(dirty, config);
而且它对 SVG、MathML 这些复杂结构也处理得不错,社区维护活跃,漏洞响应快。亲测有效,用了三年没出过事。
但这里注意我踩过好几次坑:**DOMPurify 只适用于你确实需要解析 HTML 的场景**。如果你只是想显示纯文本(比如用户昵称、标题),那完全没必要用它,反而增加 bundle 体积。更糟的是,有人误以为 “用了 DOMPurify 就绝对安全”,结果把净化后的 HTML 插进 <script> 或 href 里,照样被绕过。记住:**净化 ≠ 转义,上下文决定一切**。
框架自带的转义:省心,但有盲区
如果你用 React 或 Vue,其实大部分时候不用操心 XSS——因为它们默认就做了转义。比如 React 的 JSX:
function UserProfile({ name }) {
return <div>Hello, {name}</div>; // name 会被自动转义
}
Vue 也一样:
<template>
<div>Hello, {{ name }}</div>
</template>
这种情况下,<script> 会被当作文本显示,不会执行。所以日常开发中,只要别手贱用 dangerouslySetInnerHTML 或 v-html,基本是安全的。
但问题就出在那些“不得不渲染 HTML”的地方。比如从 CMS 拿来的富文本内容,你非得用 v-html 或 dangerouslySetInnerHTML。这时候,很多人就直接塞进去,结果埋雷。正确的做法是:**先用 DOMPurify 处理,再用 v-html**。
<template>
<div v-html="sanitizedContent"></div>
</template>
<script>
import DOMPurify from 'dompurify';
export default {
data() {
return {
content: '<img src=x onerror=alert(1)>'
};
},
computed: {
sanitizedContent() {
return DOMPurify.sanitize(this.content);
}
}
};
</script>
React 同理。所以我的建议是:**框架转义是第一道防线,但涉及动态 HTML 时,必须叠加 DOMPurify**。
我的选型逻辑
折腾了半天发现,其实没有“最好”的方案,只有“最适合当前场景”的方案。我的选择逻辑很简单:
- 如果只是插入纯文本(比如用户昵称、文章标题)→ 用框架自带转义,或者手动
escapeHTML(老项目) - 如果需要渲染用户提供的 HTML(比如评论、富文本)→ 无脑上 DOMPurify,配合严格配置
- 如果是在非标准上下文(比如
href、onclick、<script>内)→ 别插用户输入!实在不行,用白名单 + 严格校验,而不是依赖转义
另外,千万别忘了:**前端安全编码只是最后一道防线**。后端也得做同样的事,数据库存储前最好也清洗一遍。XSS 防御是纵深防御,不能只靠前端扛。
还有个小细节:有些团队会用 CSP(Content Security Policy)来兜底。这确实有用,但 CSP 不能替代编码。比如内联脚本被阻止了,但攻击者可能改用 <img src="data:image/svg+xml,..."> 触发 XSS,CSP 不一定拦得住。所以,编码 + CSP 才是组合拳。
结尾:安全不是功能,是习惯
以上是我对前端安全编码几个方案的对比总结。DOMPurify 是我目前最推荐的工具,但前提是用对场景。手动转义函数在特定环境下仍有价值,而框架的自动转义则是日常开发的“安全网”。
这个方案不是最优的,但最简单——只要你记住:**永远不要信任用户输入,永远根据输出上下文选择合适的编码方式**。
以上是我踩坑后的总结,希望对你有帮助。有不同看法或更优的实现方式,欢迎评论区交流。

暂无评论