FlawOpen

flawopen.com/xss/Javascript

● CWE-79 · 高危
安全研究 · FlawOpen

JavaScript XSS:保护 DOM、React 与 Express

不安全的 DOM/React 接口和链接可能执行 JavaScript。优先使用文本 API,清理 HTML、验证 URL 并启用 Trusted Types(CWE-79)。

💡 通俗易懂的原理解析 (ELI5)

想象一个剧场提示台。如果舞台监督把观众的每张便条都当作舞台指令,便条就可能添加意外的场景;若将它作为普通对白展示,它就只是文本。在浏览器中,innerHTML 和 React 的 dangerouslySetInnerHTML 会让 DOM 把输入解析为标记,而 textContent 会按原样显示。普通文本应转义;只对明确允许的少量 HTML 做清理;创建链接前验证 URL 协议。

核心概念与专有名词

innerHTML vs textContent
给 innerHTML、outerHTML 或 insertAdjacentHTML 赋值会让浏览器把字符串当作 HTML 解析,其中的 <img onerror> 就成了活的元素。textContent 和 createTextNode 只插入文本,值中的任何内容都不会被解析为标记。
Template literals in res.send()
Express 的 res.send() 默认以 text/html 发送字符串。模板字符串在发送前就对 ${req.query.q} 求值,因此响应里包含用户在查询字符串中放入的任何内容。req.query.q 还可能是数组或对象,所以要检查类型。
dangerouslySetInnerHTML + DOMPurify
React 会自动转义 JSX 中的文本,dangerouslySetInnerHTML 是显式的例外。把值交给 DOMPurify.sanitize(),它用浏览器自带的解析器解析内容,并在 React 插入之前删除脚本、事件处理器和危险 URL。
javascript: URLs in href
JSX 会转义属性值,但 href="javascript:..." 是合法的 URL,点击时就会执行代码。用 new URL() 解析,只允许 http: 和 https:;URL 解析器会去掉开头的空格和制表符,而简单的前缀检查会漏掉它们。
Trusted Types
启用 CSP 指令 require-trusted-types-for 'script' 后,支持的浏览器会拒绝向 innerHTML 等接收点写入普通字符串,只接受由你定义的策略(通常会调用 DOMPurify)创建的 TrustedHTML 对象。新增的接收点不会悄悄混进来。

框架映射

攻击执行流程分解

步骤 1

输入到达接收点

搜索词、从 API 加载的个人资料字段或其他用户保存的链接以字符串形式到达,并被交给 innerHTML、dangerouslySetInnerHTML、href 或 res.send()。

步骤 2

值变成了标记

使用无害的测试值 <img src=x onerror=alert(1)> 时,innerHTML 和 res.send() 会生成一个真实的 <img> 元素;保存下来的 javascript:alert(1) 链接则成为可点击的脚本。

步骤 3

浏览器执行了它

图片加载失败并触发 onerror,或者用户点击了链接。代码立即执行,攻击者无需任何后续操作。

步骤 4

脚本以网站的身份行事

它以页面的源(origin)运行:可以读取 DOM,用受害者的 cookie 或 localStorage 中的令牌调用网站 API,并篡改用户看到的内容。

源代码对比:漏洞与安全实现

存在漏洞的实现
// profile.jsx:下面每个函数都让字符串被当作 HTML 解析
import express from 'express';

const app = express();

// Express:发送之前查询值就被放进了 HTML
app.get('/search', (req, res) => {
  res.send(`<h1>Results for ${req.query.q}</h1>`);
});

// innerHTML 把 name 当作标记解析
export function showGreeting(el, name) {
  el.innerHTML = `Hello, ${name}`;
}

// React 原样插入收到的 HTML
export function Bio({ html }) {
  return <div className="bio" dangerouslySetInnerHTML={{ __html: html }} />;
}

// 点击链接时 javascript: URL 就会执行
export function Website({ url }) {
  return <a href={url}>Website</a>;
}
加固后的安全修复
// profile.jsx:文本始终是文本;HTML 和 URL 先经过检查
import DOMPurify from 'dompurify';
import escapeHtml from 'escape-html';
import express from 'express';

const app = express();

// 只接受字符串,并对其做 HTML 转义
app.get('/search', (req, res) => {
  const q = typeof req.query.q === 'string' ? req.query.q : '';
  res.send(`<h1>Results for ${escapeHtml(q)}</h1>`);
});

// textContent 从不解析标记
export function showGreeting(el, name) {
  el.textContent = `Hello, ${name}`;
}

// DOMPurify 在插入前删除脚本和事件处理器
export function Bio({ html }) {
  return <div className="bio" dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(html) }} />;
}

// 只保留 http 和 https URL,其余都变成 #
function safeHref(url) {
  try {
    const parsed = new URL(url, window.location.origin);
    return ['http:', 'https:'].includes(parsed.protocol) ? parsed.href : '#';
  } catch {
    return '#';
  }
}

export function Website({ url }) {
  return <a href={safeHref(url)}>Website</a>;
}

工程与系统安全加固清单

参考来源