安全编码实战:避开常见漏洞的实用开发技巧

博主慧玲 安全 阅读 1,998
赞 11 收藏
二维码
手机扫码查看
反馈

前端安全编码,我到底该用哪个方案?

最近在重构一个老项目,突然被安全团队扫出一堆 XSS 漏洞,主要是用户输入内容没做转义就直接插进 DOM。这事儿说大不大,说小不小,但真要线上出事,背锅的还是我们前端。于是我就花了一天时间,把主流的几种前端安全编码方案都试了一遍,踩了不少坑,也有了点自己的偏好。今天就来聊聊这几个方案的实际体验。

安全编码实战:避开常见漏洞的实用开发技巧

谁更灵活?谁更省事?

我主要对比了三种方案:手动写 escapeHTML 函数、用 DOMPurify 库、以及用 React/Vue 的内置机制(比如 JSX 自动转义)。别看都是“防 XSS”,实际用起来差别挺大。

先说最原始的——自己写个 escapeHTML 函数。这招我在早期项目里常用,代码简单,控制感强:

function escapeHTML(str) {
  if (typeof str !== 'string') return str;
  return str
    .replace(/&/g, '&')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#x27;');
}

用起来就是:

const userInput = '<script>alert("xss")</script>';
document.getElementById('output').innerHTML = escapeHTML(userInput);

优点是轻量、无依赖,适合那种“不能加任何第三方库”的老系统。但问题也很明显:**你只处理了 HTML 特殊字符,没处理上下文**。比如,如果这段内容被插进 <script> 标签里,或者作为 onclick 的属性值,光转义 &lt; 是没用的。我之前就在这栽过跟头——以为转义了就安全,结果攻击者用 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> 会被当作文本显示,不会执行。所以日常开发中,只要别手贱用 dangerouslySetInnerHTMLv-html,基本是安全的。

但问题就出在那些“不得不渲染 HTML”的地方。比如从 CMS 拿来的富文本内容,你非得用 v-htmldangerouslySetInnerHTML。这时候,很多人就直接塞进去,结果埋雷。正确的做法是:**先用 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,配合严格配置
  • 如果是在非标准上下文(比如 hrefonclick<script> 内)→ 别插用户输入!实在不行,用白名单 + 严格校验,而不是依赖转义

另外,千万别忘了:**前端安全编码只是最后一道防线**。后端也得做同样的事,数据库存储前最好也清洗一遍。XSS 防御是纵深防御,不能只靠前端扛。

还有个小细节:有些团队会用 CSP(Content Security Policy)来兜底。这确实有用,但 CSP 不能替代编码。比如内联脚本被阻止了,但攻击者可能改用 <img src="data:image/svg+xml,..."> 触发 XSS,CSP 不一定拦得住。所以,编码 + CSP 才是组合拳。

结尾:安全不是功能,是习惯

以上是我对前端安全编码几个方案的对比总结。DOMPurify 是我目前最推荐的工具,但前提是用对场景。手动转义函数在特定环境下仍有价值,而框架的自动转义则是日常开发的“安全网”。

这个方案不是最优的,但最简单——只要你记住:**永远不要信任用户输入,永远根据输出上下文选择合适的编码方式**。

以上是我踩坑后的总结,希望对你有帮助。有不同看法或更优的实现方式,欢迎评论区交流。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论