你的网站真的安全吗?XSS攻击可能正在窃取用户数据

别急着关页面——你刚上线的活动页、用户评论区、甚至那个“欢迎回来,张三”的小提示,都可能是XSS的后门。真实情况是:90%的XSS漏洞,不是出在黑客多高明,而是我们把用户输入原封不动塞进了HTML里,还信誓旦旦点了个“提交”。

为什么你的输入框会成为攻击入口?

浏览器不会质疑它渲染的内容。它只认一件事:你给它什么标签,它就照单执行。
当用户在搜索框里输入 <script>fetch('/api/me')</script>,而你又把它直接拼进HTML返回给下一个人——那这个脚本就会在对方浏览器里跑起来。
就像你让前台帮贴个便签:“张经理,速来3楼”,结果便签背面用隐形墨水写着“把保险柜密码发我”。所有路过的人,都照做了。

某招聘平台曾出过类似问题:求职者在“自我介绍”栏里藏了一段JS,只要HR点开简历预览,脚本就悄悄把当前页面的登录态发到外部服务器。后续账号被批量盗用,直到有员工发现自己的后台在没操作的情况下自动点了“导出全部候选人”。

处理用户输入,仅仅过滤够吗?

别再写 str.replace(/<script>/gi, '') 了。
攻击者早把 <ScRiPt><img/src="x"onerror=alert()>、甚至 &#60;script&#62; 玩得比你还熟。
黑名单式过滤,等于在防盗门上画一把锁。

真正管用的是两步:

  • 输入时验证:手机号字段,就只收数字、括号和短横线;邮箱字段,用标准正则校验格式;其他字段,明确长度和字符集。验证必须放在后端,前端校验只是省用户一次刷新。
  • 输出时编码:这才是防XSS的主战场。数据安安静静躺在数据库里不危险,危险的是它被塞进HTML那一刻。

输出到页面时,到底该怎么做?

记住一句话:数据放哪儿,就按那儿的规则编码

  • 如果插在 HTML 标签中间(比如 <p>用户写了:{content}</p>),就做 HTML 实体编码:
    <&lt;>&gt;&&amp;"&quot;'&#x27;
    这样 <script> 就老老实实显示成文字,不会被解析。

  • 如果塞进 HTML 属性里(比如 <input value="{content}">),除了实体编码,还得确保属性值用引号包裹。不加引号?编码可能直接失效。

  • 如果要放进 JS 字符串(比如 var name = "{content}";),就得用 JavaScript 字符串编码,比如把双引号转成 \x22;放进 URL 参数?那就用 encodeURIComponent()
    别硬编,用框架自带的输出函数(如 React 的 {content}、Vue 的 {{ content }}、Django 的 {{ content|escape }}),它们默认做了上下文感知编码。

有哪些现成的安全机制可以直接用?

别重复造轮子。你天天用的工具,早就给你备好了盾牌:

  • CSP(内容安全策略):在 Nginx 或后端加一行响应头:
    Content-Security-Policy: script-src 'self';
    这句话的意思是:“浏览器,只准执行同源的JS,别的脚本,不管藏得多深,一律不许运行。”
    即使 XSS payload 成功注入,也会被拦在执行前。

  • Cookie 加个 HttpOnly
    设置 Set-Cookie: sessionid=xxx; HttpOnly; Secure;
    这样 document.cookie 就读不到它——XSS 拿不到 session,基本就废了一半。

  • 响应头补两行保底:
    X-Content-Type-Options: nosniff(防MIME类型混淆)
    X-Frame-Options: DENY(防点击劫持)

你的富文本编辑器是不是安全盲区?

论坛、知识库、客服消息……这些地方不能简单地全量转义,否则加粗变纯文本,链接变死链。

这时候,白名单是唯一靠谱的路:

  • 只允许 <p> <b> <i> <ul> <li> <a> 这类基础标签;
  • <a> 标签只放 href,且强制检查协议必须是 https://http://,砍掉 javascript:data:
  • 所有 style 属性一概拒绝,CSS 能做的恶意事,远超你想象。

别手写正则去“修”HTML。用成熟的库:

  • 后端推荐 DOMPurify(Node)、bleach(Python)、jsoup(Java);
  • 前端直接上 DOMPurify.sanitize(html),一行代码搞定。

另外,少用 .innerHTML = xxx。能用 .textContent 显示纯文本,就别碰 HTML;非得动态加节点?用 document.createElement('div') + appendChild(),安全又干净。

如何建立持续的安全防御习惯?

安全不是上线前突击检查,而是每天写代码时的肌肉记忆:

  • 每次写模板,问自己一句:“这段变量,是从哪来的?我要把它放在 HTML 的哪个位置?”
  • 代码审查时,重点盯三处:表单接收、数据库读取、模板渲染。这三个环节串起来,就是一条完整的XSS流水线。
  • 用你 already 在用的工具扫漏洞:
    • VS Code 装上 “ESLint + eslint-plugin-security” 插件,保存即报 dangerous innerHTML
    • Jenkins 或 GitHub Actions 里加个 npm run auditpip install --upgrade pip && pip install safety && safety check
  • 每季度快速过一遍依赖:npm outdated / pip list --outdated,重点关注 expresslodashmoment 这类高频漏洞库。

今天下班前就能做的一件事

打开你正在维护的项目,在本地或测试环境里,找一个用户可编辑、且内容会直接展示的页面——比如「个人简介」编辑页、「评论列表」页、「消息通知」弹窗。

然后,在对应输入框里,粘贴这三行,逐个提交:

  1. <script>alert(1)</script>
  2. <img src=x onerror=alert(1)>
  3. "><svg/onload=alert(1)>

如果页面弹出警告框,或者排版崩了、按钮消失了、控制台报错——说明漏洞就在那儿。
不用等明天,现在就打开你项目的模板文件(比如 profile.htmlcomment.vue),找到渲染用户内容的那一行,把它改成带上下文编码的写法

  • Vue:把 {{ user.bio }} 改成 v-html="user.bio | safeHtml"(并配好过滤器);
  • React:把 {user.bio} 改成 {DOMPurify.sanitize(user.bio)}
  • Django:把 {{ user.bio }} 改成 {{ user.bio|escape }} 或更严格的 {{ user.bio|urlize|linebreaks }}

改完,重新提交测试。弹窗没了,样式稳了——你刚刚亲手堵住了一个真实存在的XSS入口。