主题
HTML:网页的骨架,也是注入的入口
上一阶段你知道了浏览器会"渲染"响应内容。这一课我们拆开渲染的原材料——HTML。它看起来只是排版记号,但 XSS 的 payload 最终就落在 HTML 里,所以这一课和安全的关系比你以为的紧得多。
一、HTML 只有三件事:标签、属性、嵌套
HTML(HyperText Markup Language,超文本标记语言)不是编程语言:它没有 if、没有循环,只是一份写给浏览器看的"结构说明书"。它由三样东西组成:
- 标签(tag):尖括号包起来的记号,例如
<p>、<img>。多数标签成对出现,</p>是它的结束标签 - 属性(attribute):写在开始标签内部,格式是
名字="值",用来给标签补充参数 - 嵌套(nesting):标签可以一层层套住,最后形成一棵树
html
<a href="https://www.myxbw.cn" target="_blank">我的知识库</a>上面这一个标签里:a 是标签名,href 决定"点一下去哪里",target="_blank" 决定"在新标签页打开",中间的中文是标签内容。读懂这一行,你已经能读懂 90% 的 HTML 了。
二、文档结构:head 管"说明",body 管"内容"
一个完整的页面长这样:
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Web 基础学习站</title>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<h1>你好</h1>
<script src="/app.js"></script>
</body>
</html>| 区域 | 作用 | 会不会显示在页面上 |
|---|---|---|
<!DOCTYPE html> | 告诉浏览器用标准模式解析 | 不显示 |
head | 页面元信息:字符集、标题、引用的 CSS | 不显示(title 显示在标签页上) |
body | 用户真正看到的所有内容 | 显示 |
<meta charset="utf-8"> 这一行你务必记住——中文乱码就是它写错了。而 <title> 是浏览器标签页上的文字,也是搜索引擎和钓鱼站最爱伪造的地方。
三、常用标签速查
| 标签 | 作用 | 和安全的关系 |
|---|---|---|
a | 超链接 | javascript: 伪协议可当 XSS 载荷 |
img | 图片 | onerror 属性可被注入执行代码;src 可外带数据 |
form | 表单 | CSRF 的发射台 |
input | 输入框 | 参数从 name/value 交给服务器 |
script | 脚本 | 直接执行代码,XSS 的终极目标 |
iframe | 内嵌另一个页面 | 点击劫持、框架注入 |
四、表单:参数是怎么交给服务器的
表单是"用户输入"进入服务器的主要通道。看一个能映射到你知识库的搜索表单:
html
<form action="/search" method="get">
<input type="text" name="q" value="">
<input type="hidden" name="from" value="home">
<input type="file" name="avatar">
<button type="submit">搜索</button>
</form>浏览器提交时会做一件事:把每个有 name 的输入框,拼成 name=value 的参数对。
name是"参数的键名",value是"参数的值"——两者都由页面作者写死或用用户输入填充method="get":参数拼到 URL 后面,变成/search?q=sql&from=homemethod="post":参数放进请求体,URL 上看不见
| 对比项 | GET | POST |
|---|---|---|
| 参数位置 | URL 查询字符串 | 请求体 |
| 会不会留在浏览器历史/日志 | 会 | 一般不 |
| 能不能传文件 | 不能 | 可以 |
| 典型用途 | 搜索、翻页 | 登录、提交、上传 |
<input type="hidden"> 值得你多看一眼:它不显示,但仍会被提交。很多站点的"用户 ID""价格""积分"就藏在这里——如果服务器相信了这个字段,攻击者改一个数字就能改价。这属于"越权"类漏洞的常见成因。
五、HTML 实体编码:< 为什么能救命
HTML 里有些字符有特殊含义,不能直接写,要用**实体(entity)**转义:
| 字符 | 实体写法 | 不转义会怎样 |
|---|---|---|
< | < | 浏览器以为是标签开始 |
> | > | 同理 |
& | & | 引起实体解析混乱 |
" | " | 可能提前闭合属性 |
道理很简单:浏览器解析 HTML 时,看到 < 就想"这里要开始一个标签了"。所以只要把用户输入里的 < 换成 <,浏览器就只会把它当普通文字显示——这就是最基础、最有效的 XSS 防御:输出编码。
六、为什么"用户输入被拼进 HTML"就会 XSS
很多老代码是这么写页面的:
html
<div class="welcome">欢迎你,<?php echo $_GET['name']; ?></div>作者的想法是"把名字填进去",但服务器不过滤、不转义。于是你在地址栏访问:
text
https://example.com/?name=<script>alert(document.cookie)</script>服务器拼出来的 HTML 就变成:
html
<div class="welcome">欢迎你,<script>alert(document.cookie)</script></div>浏览器分不清哪个 <script> 是作者写的、哪个是你塞的,于是照单执行。这就是反射型 XSS 的完整原理:不是代码有 bug 被"攻破",而是数据被当成了代码。记住这句口诀——数据的边界一旦失守,数据就变成了代码。
💡 动手实验 1:在 Edge 打开
https://www.myxbw.cn,按Ctrl+U查看网页源代码。找到<head>区域,数一数里面有几个<meta>、有几个<script>。然后按Ctrl+F在源码里搜<form,看看这个站有几个表单入口。这一步叫你建立"我现在在读的是骨架"的感觉。
七、iframe 与 script:两个最危险的标签
<script> 危险的理由不需要解释:它里面的内容会被浏览器直接执行。所以只要有办法把 <script> 送进页面,就等于拿到了页面的控制权。现代站点常做过滤,于是攻击者改用 <img src=x onerror=...> 这类"不需要 <script> 也能跑代码"的标签——这提醒你:黑名单永远堵不完,白名单编码才是正路。
<iframe> 是"页面里的窗口",可以内嵌另一个页面:
html
<iframe src="https://www.myxbw.cn" width="800" height="600"></iframe>它本身不执行代码,但它是点击劫持和框架注入的主角:攻击者把真实站点透明地盖在自己页面上,诱导你"点这里领奖",实际点的是嵌在下面的真实按钮。
你的知识库正是用响应头挡住这一招的:
text
x-frame-options: DENY这条头的含义是"任何页面都不许把我放进 iframe"。你可以在 Network 面板里亲眼确认它。顺带一个冷知识:VitePress 的 head 里也可以配 http-equiv 版本的 X-Frame-Options,但浏览器只认真实的 HTTP 响应头,meta 版本基本是装饰——这解释了为什么本站的 meta 只是提醒,真正生效的是 Vercel 上配的那条头。
💡 动手实验 2:在桌面上新建一个文本文件,改名为
frame-test.html(注意扩展名),用记事本写入下面这段,保存后双击用 Edge 打开。然后按 F12 看 Console 面板里有没有"拒绝显示"的报错。
html
<!DOCTYPE html>
<html lang="zh-CN">
<body>
<h2>我要把知识库装进 iframe:</h2>
<iframe src="https://www.myxbw.cn" width="800" height="400"></iframe>
</body>
</html>如果知识库被拒绝加载,说明 x-frame-options: DENY 正在上班;如果换了别的站能加载,说明它没配这条头——这就是你以后做安全评估时要写进报告的一行。
八、CSRF:为什么一个自动提交的表单就够
CSRF(跨站请求伪造)不要求你写 JavaScript,一个表单就够了:
html
<form action="https://bank.example.com/transfer" method="post" id="f">
<input type="hidden" name="to" value="attacker">
<input type="hidden" name="amount" value="10000">
</form>
<script>document.getElementById('f').submit();</script>
</body>原理三步:表单的 action 指向目标站点的接口;参数全部由隐藏字段写死,用户什么都不用点;页面一加载就自动提交。此时浏览器会自动带上目标站点的 Cookie(Cookie 是按域名发的,不看页面来自哪里),服务器一看凭证有效,就把钱转了。
所以 CSRF 的防御从来不在 HTML 层,而在后端和 Cookie 层:校验 Origin/Referer、使用一次性 token、给 Cookie 加 SameSite。这些我们在 JavaScript 那课 和后端阶段会接着讲。
九、HTML 与 DOM 的区别
这是初学者最容易含混的一点:
| 对比项 | HTML | DOM |
|---|---|---|
| 是什么 | 服务器发来的文本文件 | 浏览器解析后建立的内存对象树 |
| 谁在管 | 服务器 | 浏览器 + JavaScript |
| 会不会变 | 下载完就固定了 | 随时可以被 JS 改写 |
| 看它用哪里 | Ctrl+U 看源代码 | F12 的 Elements 面板 |
关键结论:你在 Elements 面板里改动的东西,不会回到服务器,也不会出现在 Ctrl+U 的源代码里。这既是调试的便利(随便改不怕弄坏线上),也是安全测试的常用手段——下一课 F12 精要 会专门用它来验证 XSS 想法。
小结
- HTML = 标签 + 属性 + 嵌套,浏览器把它解析成 DOM 树
head管元信息,body管看得见的内容- 表单通过
name=value把参数交给服务器,GET 在 URL、POST 在请求体,隐藏字段也会被提交 - 实体编码把
<变成<,让数据永远是数据——这是防 XSS 的第一道墙 - 用户输入被拼进 HTML 就会产生 XSS;
script和iframe是两个必须盯住的标签 - CSRF 只需要一个自动提交的表单,因为浏览器会自动带上目标站的 Cookie
- HTML 是文本,DOM 是内存里的活树,两者别混
下一课我们给这副骨架穿上皮肤:CSS。
📎 本课对应知识库阅读:《XSS 跨站脚本攻击》(对照文中的表单与实体编码部分,你会发现原理和你今天读到的完全一致)。