XSS概述

对于前端开发而言,应该都是知道这个漏洞的。全称为:跨站脚本(Cross Site Script),因为简写成CSS会冲突,所以叫做XSS

这个漏洞的本质其实就是通过前端提供出去的功能,可以注入JS代码,然后可以被前端执行

来一个简单的实例:

    ...
      <!-- 输入区域 -->
      <div class="input-section">
        <h2>输入测试内容</h2>
        <input
          type="text"
          id="userInput"
          placeholder="请输入内容(支持HTML/JavaScript代码)"
        />
        <button onclick="submitInput()">提交</button>
        <button onclick="clearOutput()" class="clear-btn">清除</button>
      </div>

      <!-- 输出显示区域 -->
      <div class="output-section">
        <h2>输出结果</h2>
        <div id="outputArea">
          <p class="placeholder">输出内容将显示在这里...</p>
        </div>
      </div>
    ...

    <script>
      // 核心函数:处理用户输入并显示(包含XSS漏洞)
      function submitInput() {
        const userInput = document.getElementById("userInput");
        const outputArea = document.getElementById("outputArea");
        const inputValue = userInput.value.trim();

        if (inputValue === "") {
          alert("请输入内容!");
          return;
        }

        // 关键漏洞点:直接使用innerHTML插入用户输入,未进行任何过滤
        // 这允许恶意脚本执行,造成反射型XSS漏洞
        outputArea.innerHTML =
          '<div class="output-content"><strong>您输入的内容:</strong><br>' +
          inputValue +
          "</div>";

        // 清空输入框
        userInput.value = "";
        userInput.focus();
      }

      // 清除输出内容
      function clearOutput() {
        const outputArea = document.getElementById("outputArea");
        outputArea.innerHTML =
          '<p class="placeholder">输出内容将显示在这里...</p>';
      }

      // 页面加载完成后添加键盘事件支持
      document.addEventListener("DOMContentLoaded", function () {
        const userInput = document.getElementById("userInput");

        // 支持Enter键提交
        userInput.addEventListener("keypress", function (event) {
          if (event.key === "Enter") {
            submitInput();
          }
        });

        // 输入框获得焦点
        userInput.focus();
      });
    </script>

这里只把需要用到的地方列出来了。html代码自己尝试的时候可以交给AI补全。

这就是一个输入框,然后下方还有一个输出内容的区域,从下面的前端代码可以看到,获取到内容之后,通过innerHTML属性以标签的方式插入内容,这就是一个典型的 反射XSS

通常,验证XSS的方法,就是通过alert()或者console.log()函数憨憨是否会回显进行简单的验证。

由于Chrome浏览器本身也有一定的XSS的防护机制,直接使用:

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

就可以看到弹窗了,这样,我们就证明了漏洞的存在,同时,我们F12打开开发者工具, 也可以看到img标签被写入了进去:

危害

XSS的核心危害就是通过在网站里植入恶意的JS代码,窃取用户cookie或者利用用户cookie进行未授权操作,通俗来说,就是窃取凭证,盗号。cookie是账号的唯一凭证,拿到了它就相当于拿到了账号权限,这也就是XSS漏洞的核心目标。

当然了,除了这个核心目标之外,还有一些针对于前端页面的危害:

  • 篡改页面内容,诱导用户操作,进行钓鱼攻击
  • 植入恶意的连接,对页面进行重定向
  • 通过引入外链的方式绕过内容安全审查策略,打广告或者非法宣传。具体来说就是通过<img>标签引入违规图片或者利用<a>标签引入外链。(注意区分:如果一个触发点不能执行JS代码,而是只能植入HTML标签,那么这一类的漏洞不能称之为XSS,而是称之为HTML注入)

XSS类型

对于XSS漏洞的分类,我根据 触发方式触发位置触发影响 的不同,可以有下面三种方式:

触发方式

可以分为 单次触发 长期触发 ,分别对应的是 反射型XSS存储型XSS

反射型

通过前端输入立即触发执行,直接渲染到前端里,不持久存储在服务器中。上面的那个动图就是经典的反射型XSS。经常发生在包含JS代码的恶意链接,多用于点击挟持,钓鱼等攻击场景,需要主动触发JS代码。

存储型

顾名思义,就是持久性存储在服务器数据库中的。漏洞点多为恶意代码通过POST请求写入后端数据库,每次访问该页面的时候,后端从数据库中拉取恶意数据返回,前端拿到数据后渲染的时候都会触发并且执行JS代码,属于是被动触发。

这种被动触发的攻击方式也可以叫做 路过攻击 或者 水坑攻击,在你神不知鬼不觉的时候触发了恶意代码,所以这一类XSS危害较大,影响面更广,常见于一些持久性存储的区域,比如论坛帖子。个人信息查看页面。

触发位置

可以分为 DOM渲染 和 代码触发,可以对应为 DOM型XSS非DOM型XSS

DOM型

所谓DOM节点,通俗的说就是HTML里面的标签,那么该类型的XSS的特征就是在各种标签中触发JS代码,上面的实例漏洞demo就是这样的,核心漏洞触发点就是document标签element对象中的innerHTML属性

document.createElement("X").innerHTML
document.getElementById("xx").innerHTML

通过页面逻辑创建了一个<script>标签直接进行代码执行(Chrome浏览器自身已防护该场景),或者就是使用像上述<img onerror=""> 标签中的onerror属性一样的 ,拥有可执行JS代码的属性的标签进行触发,绝大多数标签都有可以执行JS的事件属性,比如说:

onerror
onmouseover
onmouseout
onfocus
onload
onclick
onkeydown
onkeypress
onkeyup
action
onscroll
...

上面这些情况都是基于DOM节点来触发的JS代码执行,属于很常见的类型。

非DOM型

那么与之对应的,非DOM型的就是不在标签中触发的,比如说这样:

<script>
      const urlParams = new URLSearchParams(window.location.search);
      const userInput = urlParams.get("input");
      if (userInput) {
        eval(userInput);
      }
</script>

简单来说就是通过路由获取到用户输入的内容,然后直接执行用户输入的内容。

比如说我现在访问:

http://example.com/reflected_xss_demo.html?input=alert('XSS')

那么代码就会变成:

eval("alert('XSS')");

于是乎就发生了弹窗。

Self-XSS

浏览XSS分类教程时,有看到这么一个特殊的分类:Self-XSS ,这种其实都不能算安全漏洞:

举个例子👇🏻

现在有一个页面存在XSS漏洞,但是只能由你自己访问这个页面,比如说个人资料页面。其他用户无法看到这个页面,所以危害就无法扩散到其他用户,这类型顶多算代码缺陷,完全无危害,这一点从名字来看,就知道了。

但是这种缺陷如果被发现之后从安全层面角度还是要修复的,只是低优先级而已。因为你无法保证未来的增量代码逻辑肯定不会引用存在Self-XSS的页面,这个时候就有危害了,而且也不再属于Self-XSS。

所以说现实中,当一些白帽子发现了Self-XSS的时候,会努力地找其他可以影响其他用户的触发点,也可能会每隔一段时间看看是否有新增的功能可以扩大该漏洞的利用空间。不然一个Self-XSS正常是不会被SRC收录的。

漏洞实战

漏洞场景

这里我们来模拟一下简化之后的存储XSS漏洞场景——留言板系统,也是业务中比较常见的漏洞点,我们可以借此来看一下危害:

需求

我们有一个留言板应用,用户可以提交评论,这些评论会被存储在服务器的数据库中然后展示给所有的访客,但是在留言处存在了XSS漏洞。

(篇幅有限,为了展现单一漏洞危害,所以对业务逻辑进行了大量的简化)

源码

用了最基础的HTML+CSS+JS,使用localstorage来记录留言,以此来达到存储型+DOM型的XSS漏洞实例,现在灵活使用AI功能可以快速生成需要的代码:

HTML
<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>存储型XSS漏洞演示 - 留言板系统</title>
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <div class="container">
      <div class="header">
        <h1>🛡️ 存储型XSS漏洞演示</h1>
        <p>留言板系统 - 网络安全教学演示</p>
      </div>

      <div class="warning">
        ⚠️ <strong>安全警告</strong>:
        此页面仅用于教育演示目的,展示存储型XSS漏洞原理。所有用户输入都会被直接存储和渲染,存在严重的XSS安全漏洞。请勿在生产环境中使用!
      </div>

      <div class="main-content">
        <div class="section form-section">
          <h2>📝 发表留言</h2>
          <form id="commentForm">
            <div class="form-group">
              <label for="username">用户名:</label>
              <input
                type="text"
                id="username"
                name="username"
                required
                maxlength="50"
                placeholder="请输入您的用户名"
              />
            </div>
            <div class="form-group">
              <label for="comment">留言内容:</label>
              <textarea
                id="comment"
                name="comment"
                required
                maxlength="2000"
                placeholder="分享您的想法...支持HTML标签"
              ></textarea>
            </div>
            <button type="submit" class="btn">💬 发布留言</button>
            <button
              type="button"
              class="btn btn-danger"
              onclick="clearAllComments()"
            >
              🗑️ 清空所有留言
            </button>
          </form>
        </div>

        <div class="section comments-section">
          <div class="stats">
            <div class="stat-item">
              <div class="stat-number" id="commentCount">0</div>
              <div class="stat-label">总留言数</div>
            </div>
            <div class="stat-item">
              <div class="stat-number" id="xssCount">0</div>
              <div class="stat-label">检测到XSS</div>
            </div>
          </div>

          <h2>💭 留言列表</h2>
          <div id="commentsList">
            <div class="empty-state">
              <p>暂无留言,快来发表第一条留言吧!</p>
            </div>
          </div>
        </div>

        <div class="section help-section">
          <h2>🔍 XSS漏洞演示说明</h2>
          <p>
            <strong>存储型XSS原理</strong
            >:用户输入的恶意脚本被存储在数据库中(本演示使用localStorage),当用户访问页面时,恶意脚本会被执行,影响所有访问用户。
          </p>
          <p>
            <strong>本系统漏洞</strong
            >:系统不对用户输入进行任何HTML过滤,直接将用户输入存储并使用innerHTML渲染到页面中。
          </p>

          <div class="payload-examples">
            <h4>🧪 XSS测试用例(点击复制到剪贴板):</h4>

            <div class="payload-item" onclick="copyToClipboard(this)">
              <code
                >&lt;script&gt;alert('存储型XSS攻击成功!')&lt;/script&gt;</code
              >
              <button class="copy-btn">复制</button>
            </div>

            <div class="payload-item" onclick="copyToClipboard(this)">
              <code>&lt;img src=x onerror=alert('XSS via img tag')&gt;</code>
              <button class="copy-btn">复制</button>
            </div>

            <div class="payload-item" onclick="copyToClipboard(this)">
              <code
                >&lt;div
                onclick=alert('点击事件XSS')&gt;点击我&lt;/div&gt;</code
              >
              <button class="copy-btn">复制</button>
            </div>

            <div class="payload-item" onclick="copyToClipboard(this)">
              <code
                >&lt;script&gt;document.body.style.background='red'&lt;/script&gt;</code
              >
              <button class="copy-btn">复制</button>
            </div>

            <div class="payload-item" onclick="copyToClipboard(this)">
              <code
                >&lt;script&gt;document.title='页面被XSS攻击'&lt;/script&gt;</code
              >
              <button class="copy-btn">复制</button>
            </div>

            <div class="payload-item" onclick="copyToClipboard(this)">
              <code
                >&lt;style&gt;body{transform:rotate(180deg)}&lt;/style&gt;</code
              >
              <button class="copy-btn">复制</button>
            </div>
          </div>

          <p>
            <strong>🔒 安全修复方法</strong
            >:对所有用户输入进行HTML转义,使用textContent代替innerHTML,实施内容安全策略(CSP),验证和过滤用户输入。
          </p>
        </div>
      </div>
    </div>

    <script src="script.js"></script>
  </body>
</html>
JS
// 数据存储相关函数
function getComments() {
  try {
    const comments = localStorage.getItem("xss-comments");
    return comments ? JSON.parse(comments) : [];
  } catch (error) {
    console.error("读取评论数据出错:", error);
    return [];
  }
}

function saveComments(comments) {
  try {
    localStorage.setItem("xss-comments", JSON.stringify(comments));
    return true;
  } catch (error) {
    console.error("保存评论数据出错:", error);
    return false;
  }
}

// 初始化示例数据
function initializeData() {
  const comments = getComments();
  if (comments.length === 0) {
    const initialComments = [
      {
        id: 1,
        username: "系统管理员",
        comment:
          "🎉 欢迎来到存储型XSS漏洞演示系统!<br><strong>⚠️ 重要提醒</strong>:此系统故意包含XSS安全漏洞,仅用于教学演示目的。",
        timestamp: new Date().toISOString(),
        isXSS: false,
      },
      {
        id: 2,
        username: "安全研究员",
        comment:
          "这是一个很好的XSS漏洞学习案例。系统直接将用户输入插入HTML页面,没有进行任何过滤和转义。建议尝试右侧的测试用例。",
        timestamp: new Date(Date.now() - 300000).toISOString(),
        isXSS: false,
      },
      {
        id: 3,
        username: "学习者",
        comment:
          "测试HTML标签:<em>斜体文字</em> 和 <strong>粗体文字</strong> <span style='color:blue'>蓝色文字</span>",
        timestamp: new Date(Date.now() - 600000).toISOString(),
        isXSS: false,
      },
    ];
    saveComments(initialComments);
  }
}

// XSS检测函数
function detectXSS(text) {
  const xssPatterns = [
    /<script[\s\S]*?>[\s\S]*?<\/script>/gi,
    /javascript:/gi,
    /on\w+\s*=/gi,
    /<iframe/gi,
    /<object/gi,
    /<embed/gi,
    /eval\s*\(/gi,
    /expression\s*\(/gi,
  ];

  return xssPatterns.some((pattern) => pattern.test(text));
}

// 添加评论函数 - 关键漏洞点:不进行任何过滤
function addComment(username, comment) {
  const comments = getComments();
  const isXSS = detectXSS(comment) || detectXSS(username);

  const newComment = {
    id: Date.now(),
    username: username, // 直接存储,不转义 - XSS漏洞点
    comment: comment, // 直接存储,不转义 - XSS漏洞点
    timestamp: new Date().toISOString(),
    isXSS: isXSS,
  };

  comments.push(newComment);

  if (saveComments(comments)) {
    console.log("新留言已保存:", {
      username,
      comment: comment.substring(0, 50) + "...",
      isXSS,
    });
    renderComments();
    updateStats();
    return true;
  }
  return false;
}

// 渲染评论列表 - 关键漏洞点:使用innerHTML直接渲染
function renderComments() {
  const comments = getComments();
  const commentsList = document.getElementById("commentsList");

  if (comments.length === 0) {
    commentsList.innerHTML = `
      <div class="empty-state">
        <p>暂无留言,快来发表第一条留言吧!</p>
      </div>
    `;
    return;
  }

  // 按时间倒序排列
  comments.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));

  const commentsHTML = comments
    .map(
      (comment) => `
        <div class="comment-card ${comment.isXSS ? "xss-detected" : ""}">
          <div class="comment-header">
            <strong class="username">${comment.username}</strong>
            <span class="timestamp">${formatTime(comment.timestamp)} ${
        comment.isXSS ? "⚠️" : ""
      }</span>
          </div>
          <div class="comment-content">
            ${comment.comment}
          </div>
        </div>
      `
    )
    .join("");

  // 关键漏洞:直接使用innerHTML,恶意脚本会被执行
  commentsList.innerHTML = commentsHTML;
}

// 时间格式化
function formatTime(timestamp) {
  return new Date(timestamp).toLocaleString("zh-CN", {
    year: "numeric",
    month: "2-digit",
    day: "2-digit",
    hour: "2-digit",
    minute: "2-digit",
  });
}

// 更新统计信息
function updateStats() {
  const comments = getComments();
  const xssComments = comments.filter((c) => c.isXSS);

  document.getElementById("commentCount").textContent = comments.length;
  document.getElementById("xssCount").textContent = xssComments.length;
}

// 清空所有评论
function clearAllComments() {
  if (confirm("确定要清空所有留言吗?此操作不可恢复!")) {
    localStorage.removeItem("xss-comments");
    renderComments();
    updateStats();
    alert("所有留言已清空!");
  }
}

// 复制到剪贴板
function copyToClipboard(element) {
  const code = element.querySelector("code").textContent;

  if (navigator.clipboard) {
    navigator.clipboard
      .writeText(code)
      .then(() => {
        const btn = element.querySelector(".copy-btn");
        const originalText = btn.textContent;
        btn.textContent = "已复制!";
        setTimeout(() => {
          btn.textContent = originalText;
        }, 2000);
      })
      .catch((err) => {
        console.error("复制失败:", err);
        fallbackCopy(code);
      });
  } else {
    fallbackCopy(code);
  }
}

// 备用复制方法
function fallbackCopy(text) {
  const textarea = document.createElement("textarea");
  textarea.value = text;
  document.body.appendChild(textarea);
  textarea.select();
  try {
    document.execCommand("copy");
    alert("代码已复制到剪贴板!");
  } catch (err) {
    console.error("复制失败:", err);
    prompt("请手动复制以下代码:", text);
  }
  document.body.removeChild(textarea);
}

// 页面加载完成后初始化
document.addEventListener("DOMContentLoaded", function () {
  console.log("🎯 存储型XSS演示系统已加载");
  console.log("⚠️ 警告:此系统包含XSS漏洞,仅用于安全教学!");

  initializeData();
  renderComments();
  updateStats();

  // 表单提交处理
  document
    .getElementById("commentForm")
    .addEventListener("submit", function (e) {
      e.preventDefault();

      const usernameInput = document.getElementById("username");
      const commentInput = document.getElementById("comment");

      const username = usernameInput.value.trim();
      const comment = commentInput.value.trim();

      if (!username || !comment) {
        alert("请填写用户名和留言内容!");
        return;
      }

      if (addComment(username, comment)) {
        // 清空表单
        usernameInput.value = "";
        commentInput.value = "";

        // 提示用户
        if (detectXSS(comment) || detectXSS(username)) {
          alert("⚠️ 检测到潜在的XSS代码!留言已保存,请查看执行效果。");
        } else {
          alert("✅ 留言发布成功!");
        }
      } else {
        alert("❌ 发布失败,请重试!");
      }
    });

  // 添加一些CSS动画效果
  document.querySelectorAll(".comment-card").forEach((card, index) => {
    card.style.animationDelay = `${index * 0.1}s`;
    card.style.animation = "slideIn 0.6s ease-out forwards";
  });
});

// 添加键盘快捷键
document.addEventListener("keydown", function (e) {
  // Ctrl+Enter 快速提交
  if (e.ctrlKey && e.key === "Enter") {
    document.getElementById("commentForm").dispatchEvent(new Event("submit"));
  }

  // Ctrl+D 清空数据
  if (e.ctrlKey && e.key === "d") {
    e.preventDefault();
    clearAllComments();
  }
});
CSS
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

body {
  font-family: "Arial", "Microsoft YaHei", sans-serif;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  min-height: 100vh;
  padding: 20px;
  line-height: 1.6;
}

.container {
  max-width: 900px;
  margin: 0 auto;
  background: white;
  border-radius: 15px;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
  overflow: hidden;
  animation: slideIn 0.8s ease-out;
}

@keyframes slideIn {
  from {
    opacity: 0;
    transform: translateY(30px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.header {
  background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
  color: white;
  padding: 40px 30px;
  text-align: center;
  position: relative;
  overflow: hidden;
}

.header::before {
  content: "";
  position: absolute;
  top: -50%;
  left: -50%;
  width: 200%;
  height: 200%;
  background: radial-gradient(
    circle,
    rgba(255, 255, 255, 0.1) 0%,
    transparent 70%
  );
  animation: rotate 20s linear infinite;
}

@keyframes rotate {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}

.header h1 {
  font-size: 2.5em;
  margin-bottom: 10px;
  text-shadow: 0 3px 6px rgba(0, 0, 0, 0.3);
  position: relative;
  z-index: 1;
}

.header p {
  font-size: 1.1em;
  opacity: 0.95;
  position: relative;
  z-index: 1;
}

.warning {
  background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%);
  border-left: 5px solid #ff6b6b;
  color: #8b4513;
  padding: 20px;
  margin: 20px;
  border-radius: 10px;
  box-shadow: 0 4px 15px rgba(255, 107, 107, 0.2);
}

.warning strong {
  color: #d63031;
  font-size: 1.1em;
}

.main-content {
  padding: 30px;
}

.section {
  margin-bottom: 30px;
  padding: 25px;
  border-radius: 12px;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
}

.form-section {
  background: linear-gradient(135deg, #f8f9ff 0%, #e3f2fd 100%);
  border: 1px solid #e1f5fe;
}

.comments-section {
  background: linear-gradient(135deg, #fff9e6 0%, #fff3e0 100%);
  border: 1px solid #ffe0b3;
}

.help-section {
  background: linear-gradient(135deg, #f3e5f5 0%, #e8eaf6 100%);
  border: 1px solid #d1c4e9;
}

.section h2 {
  color: #333;
  margin-bottom: 20px;
  font-size: 1.8em;
  display: flex;
  align-items: center;
  gap: 10px;
}

.form-group {
  margin-bottom: 20px;
}

.form-group label {
  display: block;
  margin-bottom: 8px;
  color: #555;
  font-weight: 600;
  font-size: 1.1em;
}

.form-group input,
.form-group textarea {
  width: 100%;
  padding: 15px 18px;
  border: 2px solid #ddd;
  border-radius: 10px;
  font-size: 15px;
  transition: all 0.3s ease;
  background: white;
}

.form-group input:focus,
.form-group textarea:focus {
  outline: none;
  border-color: #667eea;
  box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.15);
  transform: translateY(-2px);
}

.form-group textarea {
  resize: vertical;
  min-height: 120px;
  font-family: inherit;
}

.btn {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: white;
  padding: 15px 30px;
  border: none;
  border-radius: 10px;
  font-size: 16px;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.3s ease;
  margin-right: 10px;
  box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
}

.btn:hover {
  transform: translateY(-3px);
  box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
}

.btn-danger {
  background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
  box-shadow: 0 4px 15px rgba(255, 107, 107, 0.3);
}

.btn-danger:hover {
  box-shadow: 0 8px 25px rgba(255, 107, 107, 0.4);
}

.comment-card {
  background: white;
  border: 1px solid #e9ecef;
  border-radius: 12px;
  padding: 20px;
  margin-bottom: 20px;
  transition: all 0.3s ease;
  position: relative;
  overflow: hidden;
}

.comment-card::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  width: 4px;
  height: 100%;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}

.comment-card:hover {
  transform: translateY(-2px);
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
}

.comment-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 15px;
  padding-bottom: 10px;
  border-bottom: 1px solid #eee;
}

.username {
  color: #667eea;
  font-size: 1.2em;
  font-weight: 600;
}

.timestamp {
  color: #999;
  font-size: 0.9em;
}

.comment-content {
  color: #333;
  line-height: 1.7;
  word-wrap: break-word;
  font-size: 1.05em;
}

.comment-count {
  color: #667eea;
  font-weight: 600;
}

.empty-state {
  text-align: center;
  color: #999;
  font-style: italic;
  padding: 50px 20px;
  background: white;
  border-radius: 12px;
  border: 2px dashed #ddd;
}

.empty-state::before {
  content: "💬";
  font-size: 3em;
  display: block;
  margin-bottom: 15px;
}

.payload-examples {
  background: white;
  padding: 20px;
  border-radius: 10px;
  margin-top: 20px;
  border: 1px solid #ddd;
}

.payload-examples h4 {
  color: #333;
  margin-bottom: 15px;
  font-size: 1.2em;
}

.payload-item {
  background: #f8f9fa;
  padding: 12px 15px;
  margin: 10px 0;
  border-radius: 8px;
  border-left: 4px solid #667eea;
  position: relative;
}

.payload-item code {
  font-family: "Consolas", "Monaco", "Courier New", monospace;
  color: #e91e63;
  word-break: break-all;
  font-size: 14px;
  background: none;
  padding: 0;
}

.payload-item .copy-btn {
  position: absolute;
  top: 50%;
  right: 10px;
  transform: translateY(-50%);
  background: #667eea;
  color: white;
  border: none;
  border-radius: 5px;
  padding: 5px 10px;
  font-size: 12px;
  cursor: pointer;
  opacity: 0.7;
  transition: opacity 0.3s;
}

.payload-item:hover .copy-btn {
  opacity: 1;
}

.stats {
  display: flex;
  gap: 20px;
  margin-bottom: 20px;
}

.stat-item {
  background: white;
  padding: 15px 20px;
  border-radius: 10px;
  text-align: center;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  flex: 1;
}

.stat-number {
  font-size: 2em;
  font-weight: bold;
  color: #667eea;
}

.stat-label {
  color: #666;
  font-size: 0.9em;
}

@media (max-width: 768px) {
  .container {
    margin: 0;
    border-radius: 0;
  }

  .header {
    padding: 30px 20px;
  }

  .header h1 {
    font-size: 2em;
  }

  .main-content {
    padding: 20px;
  }

  .section {
    padding: 20px;
  }

  .comment-header {
    flex-direction: column;
    align-items: flex-start;
    gap: 8px;
  }

  .stats {
    flex-direction: column;
  }

  .btn {
    width: 100%;
    margin-bottom: 10px;
    margin-right: 0;
  }
}

重点在于JS部分,最危险的地方有两点,一个是后续直接使用了innerHTML,一个是在定义newComment的时候,也没有特别处理内容。

这里就容易被XSS攻击。

当然了,这个实例并没有做成那种还有登录注册系统的,但是你可以想象一下,如果有多用户登录,登陆之后就可以留言,然后留言内容每一个人都可以看到,那么漏洞的影响范围就拉满了。作为攻击者,在留言的时候,写入Payload就完事儿了。

动图的内容只是一个可以点击的div块元素,如果上传者上传的是一个img标签,而src无效,那么就会触发onerror,例如onerror里面有hack行为呢?深入的行为和利用就暂时不讨论了,仅仅从直观上演示的话,当JS代码可以被执行的时候,那基本上干什么都可以了。(当然,现代互联网Chrome浏览器之类的本身也有很多的防护策略,来给攻击增加门槛)

漏洞修复

如我上面所分析的漏洞点,修复的第一要素就是让标签不要生效,一般有很多种修复方式:

  1. 不要用innerHTML或者document.write(),可以用innerText、textContent代替,上述代码中在获取数据插入留言表的时候,就是因为用的innerHTML才导致的问题
  2. 对输出展示的特殊字符进行HTML编码转义,比方说:

    const escapeHTML = (str: string): string => {
      return str
        .replace(/&/g, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/"/g, "&quot;")
        .replace(/'/g, "&#039;");
    };
    
    const msg = "<script>alert('XSS')</script>";
    const li = document.createElement("li");
    li.innerHTML = escapeHTML(msg);
  3. 对于上文提到的非DOM类型的,减少危险函数的使用
  4. 不要相信前端的输入,还需要后端进行严格的输入检查,避免出现非法字符
  5. 针对于HTML转义的修复方案,其实现在很多框架比如Vue和React都是内置了这一类安全防护能力,使用框架开发的好处就是这样
  6. 本篇说到的这一类攻击其实已经在现代社会很少见了,仅作为科普,浏览器的安全防护措施,可以后面再谈

总结

XSS的核心就是前端缺陷导致的用户输入JS可以被执行,依赖于标签,依赖于代码逻辑,只要控制住了输入和输出,该转义就转义,不要去用一些危险的函数,在非必要情况下禁用特殊字符,后端也做一下校验就好。

最后修改:2025 年 07 月 02 日
收款不要了,给孩子补充点点赞数吧