浏览器 Web 安全基础系列

  1. 浏览器怎么判断是不是同一个网站

  2. Cookie 为什么会自动携带

  3. Session 登录状态为什么可能被劫持

  4. HTTPS 为什么仍然不够

  5. CORS 到底在限制什么

  6. CSRF 是怎么冒用登录状态的

  7. CSRF 如何防御

  8. XSS 为什么能在你的网站执行(当前篇)

  9. CSP 与浏览器安全响应头

  10. Clickjacking 点击劫持

  11. 第三方资源为什么危险

  12. 防盗链到底防什么

XSS 的本质是:攻击者控制的数据被浏览器当成网页代码执行。恶意脚本一旦运行在 https://example.com 页面中,就拥有该 Origin 下普通脚本能拥有的权限,可以读取页面数据、localStorage、CSRF Token,并以当前用户身份调用接口。

三类 XSS

存储型 XSS

评论系统直接把用户输入拼进 HTML:

res.send(`<div class="comment">${comment.content}</div>`);

攻击者提交:

<img src=x onerror="alert('XSS')">

内容被保存进数据库,其他用户查看评论时执行。这类漏洞可以持续影响大量用户。

反射型 XSS

搜索页把 URL 参数原样返回:

app.get('/search', (req, res) => {
  res.send(`<h1>搜索:${req.query.q}</h1>`);
});

攻击者构造带恶意参数的链接并诱导用户点击。数据没有长期存储,而是在当前响应中“反射”执行。

DOM 型 XSS

服务器响应本身可能固定,漏洞发生在前端:

const value = location.hash.slice(1);
document.querySelector('#result').innerHTML = value;

# 后的片段通常不会发送到服务器,但前端把它作为 HTML 插入 DOM 后仍可执行。

最重要的防御:按上下文输出编码

普通文本进入 HTML 文本节点时,应转义特殊字符:

function escapeHtml(value) {
  return String(value)
    .replaceAll('&', '&amp;')
    .replaceAll('<', '&lt;')
    .replaceAll('>', '&gt;')
    .replaceAll('"', '&quot;')
    .replaceAll("'", '&#39;');
}

但 HTML、JavaScript、CSS、URL 属性的上下文规则不同。不要以为一个 HTML 转义函数能安全处理所有位置。例如:

<a href="用户输入">链接</a>
<script>const x = '用户输入'</script>
<style>.x { background: 用户输入 }</style>

实际项目应优先使用自动转义模板和框架默认渲染能力,避免手工拼接代码上下文。

安全 DOM API

危险:

element.innerHTML = userInput;
document.write(userInput);

显示普通文本时使用:

element.textContent = userInput;

创建 DOM 时明确设置属性:

const link = document.createElement('a');
link.textContent = title;
link.href = safeUrl;

URL 仍需验证协议,避免 javascript: 等危险方案:

function normalizeHttpUrl(input) {
  const url = new URL(input, location.origin);
  if (!['http:', 'https:'].includes(url.protocol)) throw new Error('invalid protocol');
  return url.href;
}

富文本必须清洗

文章编辑器需要保留 <p><strong><a> 等标签,不能全部转义。这时应使用成熟 Sanitizer 白名单清洗:

const clean = DOMPurify.sanitize(dirtyHtml, {
  ALLOWED_TAGS: ['p', 'strong', 'em', 'a', 'code', 'pre'],
  ALLOWED_ATTR: ['href', 'title']
});

不要用正则删除 <script>。XSS 载体不只 <script>,还有事件属性、SVG、危险 URL、模板语法和浏览器解析差异。

XSS 防御链

React、Vue 默认安全吗

框架通常会转义文本插值:

<div>{userInput}</div>

Vue 的 { value } 也会按文本处理。但主动使用 HTML 注入 API 会重新承担风险:

<div dangerouslySetInnerHTML={{ __html: userInput }} />
<div v-html="userInput"></div>

这类接口只能接收经过可信 Sanitizer 处理的内容。

HttpOnly 能做什么

HttpOnly 阻止 XSS 通过 document.cookie 直接读 Session Cookie,但恶意脚本仍可以:

fetch('/api/change-email', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ email: 'attacker@example.com' })
});

浏览器会自动带上 HttpOnly Cookie。因此 HttpOnly 是减损措施,不是 XSS 修复方案。

为什么 XSS 常能绕过 CSRF

恶意脚本已经运行在正常 Origin,可以读取 DOM 中的 CSRF Token、访问同源 Token 接口,并发送带正确 Origin 的请求。防 CSRF 和防 XSS 必须同时做。

CSP 的角色

CSP 可以限制脚本来源、阻止未经授权的内联脚本和 eval,但它是第二道防线。正确的输出编码、DOM API 和富文本清洗仍是第一道防线。

常见误区

输入时过滤一次就安全

同一份数据可能进入 HTML、URL、JavaScript 等不同上下文,应该在输出点按上下文处理。

只过滤 script 标签

事件属性、SVG、危险 URL 等同样能执行代码。

前端过滤即可

攻击者可以绕过前端直接请求后端。存储和输出链路都要明确数据用途;服务端渲染必须安全编码。

小结

XSS 不是简单的“弹窗漏洞”,而是攻击代码获得本站脚本权限。普通文本用自动转义和 textContent,富文本用成熟 Sanitizer,减少危险 DOM API,再配合 CSP、HttpOnly 和安全审计降低影响。

参考资料