好的!下面把 “XSS 风险演示(教学用)” 拆成一个最小可运行组件:xss-demo.html、xss-demo.css、xss-demo.js,并逐行中文注释。 效果:正常文本会被插入;当输入包含 <script>alert(1)</script> 等脚本时,在“不安全渲染”区域会弹出对话框(课堂可视化示范 XSS 危害),而“安全渲染”区域不会执行脚本。

⚠️仅限本地课堂环境演示,请勿部署到线上或用于任何真实系统。代码里已用大写 DANGER 标注了刻意不安全的部分。


1) xss-demo.html

<!DOCTYPE html> <!-- 声明 HTML5 文档类型,启用标准模式 -->
<html lang="zh-CN"> <!-- 页面主语言为中文 -->
<head>
  <meta charset="UTF-8"> <!-- 防止中文乱码 -->
  <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- 移动端友好 -->
  <title>XSS 风险演示(教学用)</title> <!-- 浏览器标签标题 -->
  <link rel="stylesheet" href="xss-demo.css"> <!-- 引入页面样式 -->
</head>
<body>
  <!-- 外层卡片容器:仅用于视觉布局,便于和你截图风格一致 -->
  <section class="card"> <!-- 模块卡片 -->
    <h2>XSS 风险演示(教学用)</h2> <!-- 模块标题 -->

    <!-- 表单区:昵称 + 任意输入 + 提交按钮 -->
    <div class="row"> <!-- 横向布局容器 -->
      <input id="nick" class="inp" type="text"
             placeholder="昵称,例如 Alice"> <!-- 昵称输入框 -->
      <input id="payload" class="inp" type="text"
             placeholder="输入 <script>alert(1)</script> 观察行为"> <!-- 可能含脚本的输入 -->
      <button id="submit" class="btn btn-danger">提交</button> <!-- 提交按钮(红色) -->
    </div>

    <!-- 不安全渲染区:故意把用户输入当成 HTML 注入 -->
    <p class="label">不安全渲染(故意演示风险):</p> <!-- 区域说明 -->
    <div id="unsafe" class="box"></div> <!-- 注入区:将通过 innerHTML + 执行脚本 -->

    <!-- 安全渲染区:仅以纯文本形式显示用户输入 -->
    <p class="label">安全渲染(textContent/转义):</p> <!-- 区域说明 -->
    <div id="safe" class="box"></div> <!-- 安全区:永不执行脚本 -->

    <!-- 教学说明:提醒 script 经 innerHTML 多数浏览器不自动执行 -->
    <p class="note">
      说明:现代浏览器通常不会自动执行通过 <code>innerHTML</code> 注入的
      <code>&lt;script&gt;</code>。为便于课堂演示,本示例在“不安全渲染”中
      还 **刻意** 执行了注入的脚本,请勿在真实网站使用。
    </p>
  </section>

  <script src="xss-demo.js"></script> <!-- 引入交互脚本 -->
</body>
</html>


2) xss-demo.css

/* ===== 基础排版与主题色 ===== */
* { box-sizing: border-box; }                 /* 统一盒模型,padding/border 计入尺寸 */
body {
  margin: 0;                                  /* 去除默认外边距 */
  background: #fff;                           /* 白色背景 */
  color: #212529;                             /* 主体文字颜色 */
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Noto Sans SC", Arial, sans-serif;
  line-height: 1.6;                           /* 提升可读性 */
  padding: 24px;                              /* 页面留白 */
}

/* ===== 卡片容器与标题 ===== */
.card {
  max-width: 1100px;                          /* 限宽便于阅读 */
  margin: 0 auto;                             /* 居中显示 */
  padding: 20px;                              /* 内边距 */
  border: 1px solid #dee2e6;                  /* 边框 */
  border-radius: 12px;                        /* 圆角 */
  background: #fff;                           /* 白底 */
}
.card > h2 { margin: 0 0 16px; font-size: 28px; }

/* ===== 表单区布局 ===== */
.row {
  display: grid;                              /* 使用网格布局 */
  grid-template-columns: 1fr 1fr auto;        /* 两个输入 + 一个按钮 */
  gap: 12px;                                  /* 控件之间的间距 */
  margin-bottom: 12px;                        /* 与下方内容的间距 */
}
.inp {
  height: 44px;                               /* 输入框高度 */
  padding: 0 12px;                            /* 内边距 */
  border: 1px solid #ced4da;                  /* 边框 */
  border-radius: 8px;                         /* 圆角 */
  font-size: 16px;                            /* 字号 */
}
.btn {
  height: 44px;                               /* 按钮高度 */
  padding: 0 18px;                            /* 左右内边距 */
  border-radius: 8px;                         /* 圆角 */
  border: 1px solid #dc3545;                  /* 红色边框 */
  background: #dc3545;                        /* 红底 */
  color: #fff;                                /* 白字 */
  cursor: pointer;                            /* 手型光标 */
}
.btn:active { transform: translateY(1px); }   /* 点击微动效 */
.btn-danger { /* 留空:已用红色主题 */ }

/* ===== 输出区样式 ===== */
.label { margin: 12px 0 8px; font-weight: 600; } /* 小标题 */
.box {
  min-height: 72px;                           /* 给出可视区域高度 */
  border: 1px solid #e5e7eb;                  /* 边框 */
  border-radius: 8px;                         /* 圆角 */
  padding: 12px;                              /* 内边距 */
  background: #f8f9fa;                        /* 浅灰底 */
  overflow-wrap: anywhere;                    /* 长串自动断行 */
  margin-bottom: 12px;                        /* 与下一个模块间距 */
}
.note {
  color: #6c757d;                             /* 注释文字色 */
  font-size: 14px;                            /* 字号稍小 */
}


3) xss-demo.js

// ====== 教学警示 ======
// 下面的 DANGER_* 逻辑是“故意不安全”的做法,用来模拟现实项目中的
// 错误用法,从而触发弹窗以示风险。请务必只在本地课堂环境使用!
const DANGER_EXECUTE_INJECTED_SCRIPT = true; // 打开后会执行用户注入的 <script> 内容

// ====== 1. 获取 DOM 元素 ======
const nickEl    = document.getElementById('nick');     // 昵称输入框
const payloadEl = document.getElementById('payload');  // 可能包含脚本的输入
const submitBtn = document.getElementById('submit');   // 提交按钮
const unsafeBox = document.getElementById('unsafe');   // 不安全渲染区域(演示风险)
const safeBox   = document.getElementById('safe');     // 安全渲染区域(纯文本显示)

// ====== 2. 绑定提交事件 ======
submitBtn.addEventListener('click', () => {            // 点击“提交”时触发
  // 2.1 读取用户输入
  const nick    = nickEl.value.trim() || '同学';       // 昵称,留空时显示“同学”
  const payload = payloadEl.value;                     // 可能包含脚本的原始字符串(不做任何处理)

  // 2.2 先清空两个展示区域
  unsafeBox.innerHTML = '';                            // 清空不安全区(HTML 注入)
  safeBox.textContent = '';                            // 清空安全区(纯文本)

  // 2.3 组合一段“欢迎语 + 用户输入”的 HTML 片段(故意把用户输入当成 HTML)
  const html = `
    <div>你好,<strong>${nick}</strong>!</div>
    <div>你输入的内容:${payload}</div>
  `;                                                   // 这段字符串中若含 <script> 将进入 DOM

  // ====== 不安全渲染(故意演示风险) ======
  unsafeBox.innerHTML = html;                          // 直接使用 innerHTML 注入(危险:XSS 源头之一)

  // 说明:现代浏览器把 innerHTML 注入的 <script> 通常不会立刻执行,
  // 为了课堂可视化,我们额外做一步“**刻意**执行注入脚本”的错误示范:
  if (DANGER_EXECUTE_INJECTED_SCRIPT) {                // 若开启“危险演示”
    // 2.4 提取注入区中所有 <script> 标签
    const scripts = unsafeBox.querySelectorAll('script'); // 选出注入的脚本
    scripts.forEach(s => {
      try {
        // 使用 Function 构造器执行脚本文本 —— 这是“极度不安全”的反面示范!
        // 目的仅为课堂上让 <script>alert(1)</script> 等能弹出对话框。
        new Function(s.textContent)();                 // DANGER: 执行任意代码
      } catch (err) {
        console.error('执行注入脚本失败:', err);      // 控制台记录错误,便于老师说明
      }
    });
  }

  // ====== 安全渲染(正确做法之一) ======
  // 思路:把用户输入当作纯文本输出,而不是 HTML。可选方案:统一转义后再拼到 innerHTML。
  // 这里用 textContent,确保任何标签/脚本都不会被当 HTML 解析。
  safeBox.textContent = `你好,${nick}!你输入的内容:${payload}`;

  // ====== 扩展(可选):给出一个“事件型”payload 的提示 ======
  // 如果学生输入:<img src=x onerror="alert('XSS')">
  // 浏览器会在不安全区加载失败并触发 onerror,从而无需上面的危险执行也会弹窗。
});


课堂讲解要点(给老师)

需要我把它再打包成一个“前端演示”子页、或给出一版“纯前端转义函数(不使用 textContent)”的教学扩展吗?