好的!下面把 “XSS 风险演示(教学用)” 拆成一个最小可运行组件:xss-demo.html、xss-demo.css、xss-demo.js,并逐行中文注释。
效果:正常文本会被插入;当输入包含 <script>alert(1)</script> 等脚本时,在“不安全渲染”区域会弹出对话框(课堂可视化示范 XSS 危害),而“安全渲染”区域不会执行脚本。
⚠️仅限本地课堂环境演示,请勿部署到线上或用于任何真实系统。代码里已用大写 DANGER 标注了刻意不安全的部分。
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><script></code>。为便于课堂演示,本示例在“不安全渲染”中
还 **刻意** 执行了注入的脚本,请勿在真实网站使用。
</p>
</section>
<script src="xss-demo.js"></script> <!-- 引入交互脚本 -->
</body>
</html>
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; /* 字号稍小 */
}
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,从而无需上面的危险执行也会弹窗。
});
innerHTML/模板拼接是常见漏洞来源;textContent/innerText、对用户输入做 HTML 转义、使用可信模板引擎的自动转义;innerHTML 直接插入的 <script>,但事件属性(如 onerror)或危险的脚本拼接仍可执行。new Function()/eval() 在本示例中仅为课堂演示,真实项目严禁使用。innerHTML”,并设计 2\~3 个 payload(如事件型、样式逃逸)测试页面安全性。需要我把它再打包成一个“前端演示”子页、或给出一版“纯前端转义函数(不使用 textContent)”的教学扩展吗?