5.4 HTML 安全


5.4 HTML 安全

本节摘要:Web 安全的头号常客是 XSS——攻击者把恶意代码注入你的页面,以你的身份在用户的浏览器里执行。本节讲清注入的三条路径(存储型、反射型、DOM 型)、根因(不可信数据被当作代码解析)与防御组合(输出转义为铁律、CSP 为纵深、最小权限为底色),并过一遍超链接与表单相关的加固清单。

学习目标

阅读完本节,你应当能够:

  1. 区分 XSS 的三种注入路径与各自的入口;
  2. 说明转义的原理:为什么转义能挡住注入;
  3. 配置一份基础 CSP 并解释各指令的含义;
  4. 落实 a 标签、iframe、表单的加固清单;
  5. 用"数据与代码分离"的视角审视各类注入问题。

注入的本质:数据被当成代码

一切注入攻击共享同一个根因:本该是数据的字符串,被放进了会被当代码解析的位置。第 5.2 节埋的线在这里展开:innerHTML 把字符串按 HTML 解析,字符串里的标签就成了活的节点;同理,把用户输入拼进脚本代码、拼进 URL 的脚本伪协议位置,都是把数据递给了代码解释器。

XSS(跨站脚本)按注入路径分三型:

存储型:恶意内容被保存进服务器(一条含脚本代码的评论),之后每个访问页面的用户都被执行。杀伤最大,因为一次注入全体命中。

反射型:恶意内容藏在请求里(一个链接的查询参数),服务器把它原样拼回响应页面。受害者需要点开攻击者构造的链接,常配合钓鱼短链投递。

DOM 型:全程不经过服务器——页面自己的脚本把不可信来源的数据(URL 片段、输入框、存储)写进 innerHTML 或 eval 类接口。前两型靠服务端转义防御,DOM 型只能靠页面脚本自己的纪律。

<!-- DOM 型的现场还原 --> <div id="greet"></div> <script> // 从地址栏片段读用户名并"欢迎"——片段里的内容完全由访问者控制 const name = decodeURIComponent(location.hash.slice(1)); document.getElementById('greet').innerHTML = '欢迎,' + name; </script> <!-- 访问该页并在地址末尾加上 #<img src=x onerror=alert(1)>,脚本执行了 -->

防线一:转义,无条件的铁律

转义的原理回到第 1 章的字符实体:把 < 变成 &lt; 后,这段内容在 HTML 解析器眼里永远是"一个小于号字符",永远不可能是"标签的开始"。数据被强制留在数据层,解释器无路可走——这就是转义能根治注入的原因,不是黑名单碰运气,是让攻击载荷在语法层面失效。

铁律的完整表述:所有不可信数据在进入危险位置前,按目标位置的语法转义。进 HTML 文本转 HTML 实体、进属性值转属性语法(含引号)、进 URL 做地址编码。三个位置三种转义,不能混用——这是工程实现里最常出错的地方,用成熟的模板引擎与转义库(它们默认转义、明确放行才输出原文)比手写转义可靠。

第 5.2 节的纪律是铁律的前半段:不可信内容一律 textContent(浏览器替你转义),innerHTML 只收自己写死的模板。

防线二:CSP,纵深的好墙

内容安全策略(CSP)是第二道防线:通过响应头或 meta 声明"本页面只允许从这些来源加载脚本、只允许这些执行方式"。它的价值在于纵深——即使转义遗漏一处,CSP 让注入的代码没有执行环境

<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; img-src 'self' data:">

三条指令的语义:默认所有资源只许同源;脚本额外收紧到同源(禁了内联脚本与第三方域);图片放开到同源加 data 协议(第 2 章图示常用的小图内嵌)。CSP 生效后,注入的 onerror 属性代码、外域脚本统统被浏览器拒绝执行,攻击链断在最后一步。

CSP 的落地痛点是排错:收紧内联脚本后,页面里历史遗留的行内事件与内联脚本块全部失效。工程节奏是先用"仅报告"模式跑一段(违规只上报不拦截),清理完违规再切换强制模式——与第 4.4 节验证的"递减曲线"同一思路:安全加固也要有可执行的过渡路径。

加固清单:a、iframe 与表单

a 标签两件事:动态生成链接时校验协议——只放行预期的协议头,拒绝脚本伪协议与未知协议(用户内容生成链接的场景必备);外站链接加反导航保护属性,防止"被链接的页面通过窗口对象反过来操控你的页面"。

iframe:嵌入第三方内容时务必加沙箱属性(sandbox),按需放开能力(允许脚本、允许同源、允许表单都是白名单式授权)——默认全禁、逐项放行,与 CSP 的白名单哲学一致。永远不用 iframe 嵌自家敏感页(同源 iframe 的隔离是假的)。

表单:target 与 action 的完整性(表单提交地址被篡改是钓鱼的老路);敏感表单必须走加密通道;浏览器自动填充的凭据字段命名规范,避免被恶意页面诱导填充。表单校验前后端都要做——前端校验是体验(第 2 章),后端校验是安全,前端永远可以被绕过。

05-04-fig01

威胁面不止 XSS

XSS 是主角但非全部,HTML 相关的威胁面再点四个角落。点击劫持:恶意页面把你的页面透明叠在下层,诱骗用户点到你的按钮——反导航保护属性与"禁止被 iframe 嵌套"的响应头(frame 系指令可写进 CSP)是标准解。开放重定向:你的跳转接口不校验目标,被滥用把用户送去钓鱼站。依赖供应链:第三方脚本被篡改就等于直接往你页面注入代码——CSP 限源、自托管关键依赖、锁定版本号,三件事各挡一段。提示安全:浏览器扩展与密码管理器读取表单,敏感页面的字段命名与自动填充关闭要刻意设计。安全的地板 mindset:你控制不了用户环境,能控制的是自己页面的每一个解析点。

会话劫持:XSS 的真正猎物

理解攻击的"收益端"能让防御更有方向感。攻击者往页面注入代码,图的是什么?头号目标是会话凭证——用户登录后浏览器持有的身份令牌。注入的脚本与页面同源运行,能读到页面能读的一切:携带凭证发起转账、以用户身份发帖、把凭证发到攻击者的服务器。这就是"跨站脚本"名字里"跨站"的含义:攻击者的代码跑在了你的站的身份证下。由此推出防御的优先级:凡是展示用户内容、处理 URL 参数、渲染富文本的位置,都是会话暴露面,转义优先级最高;静态内容、内部配置页的风险天然低一档。安全资源永远有限,按暴露面分配。

另一个高频关联概念是同源策略:浏览器限制不同源的页面互访数据,这是平台安全的基石之一。XSS 之所以恶劣,正因为它让攻击代码"获得了同源身份",绕开了同源策略的全部保护——理解这一层,就理解了为什么 XSS 被列为 Web 头号漏洞,也理解了 CSP 的设计逻辑(拆执行环境就是拆"获得同源身份"的通道)。

动手实验:注入与转义的对照

<!DOCTYPE html> <html lang="zh-CN"> <head><meta charset="UTF-8"><title>转义实验</title></head> <body> <h1>转义对照</h1> <div id="unsafe"></div> <div id="safe"></div> <script> // 模拟"用户输入":一条恶意的评论内容 const userInput = '<img src=x onerror="document.body.style.background=\'#c0392b\'">'; // 危险写法:innerHTML 直接插入,onerror 执行了,页面变红 document.getElementById('unsafe').innerHTML = '评论: ' + userInput; // 安全写法:textContent 永远按文字显示 document.getElementById('safe').textContent = '评论: ' + userInput; </script> </body> </html>

打开页面:上面的容器触发了注入(背景变红,证明代码执行了),下面的容器把这串内容原样显示为文本。两行代码的差别就是安全与失守的差别。再把 unsafe 那行改成手工转义(把小于号替换为实体写法)后插 innerHTML,验证"转义后即使走 innerHTML 也安全"——这正是服务端模板转义的原理演示。

⚠️ 常见坑:以为"我信任这个来源就不用转义"。来源的信任是会变的:存储型攻击正是借你信任的通道(评论表单)进来,再从你信任的存储(数据库)读出。转义的判断依据不是"数据从哪来",而是"数据进哪个位置"——位置危险,转义无条件。

高频疑问两则

收束一句:安全的全部要义是让数据永远是数据、代码永远是代码——两者永不越界,注入就无处发生。

问:转义会不会把正常内容弄乱?
不会,只要转义对了位置。转义后的字符串在最终显示时仍是原字符(实体是写法不是内容),用户看到的还是小于号本身。显示被弄乱只有一种情况:用了错误位置的转义(比如把 URL 转义用在了文本位置),这又回到"按目标位置语法转义"的铁律。

问:CSP 会不会影响性能?
几乎没有,它只是一份浏览器执行的策略清单。真正的成本是迁移期的排错工时——历史页面里散落的内联脚本要清理。这笔工时迟早要付(内联脚本本身就是要清理的技术债),CSP 只是给了它一个截止日期。

问:转义应该在哪一层做,前端还是后端?
按"数据最终进入渲染的位置"分工:服务端渲染的页面,转义在服务端模板层做(模板引擎默认转义);纯前端渲染,转义在脚本插入内容的位置做(textContent 或转义函数);两层都有渲染的混合应用,两层各自守自己的出口。切忌"两层都以为对方做了"——安全职责的边界必须显式写进约定,默认的默契就是漏洞的温床。

本节要点回顾

(安全的最大敌人是侥幸心理——转义多写一行不亏,注入漏一处全盘皆输;把铁律写进模板与代码评审的默认动作里,让正确成为不假思索的习惯,这笔账永远划算。)

  • 注入根因一句话:数据被放进会被当代码解析的位置。
  • 三型 XSS:存储型借保存通道、反射型借请求回显、DOM 型纯靠页面脚本;前两型服务端转义,DOM 型靠脚本纪律。
  • 转义是铁律不是最佳实践:按目标位置语法转义,数据永远留在数据层。
  • 转义分工要显式约定:服务端模板、前端脚本各守各的出口,默认默契就是漏洞温床。
  • CSP 是纵深:白名单限源、拆掉执行环境;先报告模式再强制,递减曲线落地。
  • XSS 的猎物是会话身份:注入代码获得同源身份,绕开同源策略——按暴露面分配防御资源。
  • 加固清单:动态链接验协议、iframe 加沙箱、表单前后端双重校验。

安全的最后一条是流程性的:把本节的检查项(转义纪律、CSP 头、协议白名单)写进上线的发布检查单,与第 4 章的四维验收并列。安全的特殊性在于它没有"逐步改善"的观感——漏洞要么堵上要么没有,因此检查项必须是清单式的一次性确认,而不能指望"慢慢变好"。

  • 威胁面全景:劫持、重定向、供应链——控制你能控制的每个解析点。

下一节暂时离开攻防与加固,抬头看看标准本身的下一站在哪里。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U