HTML转义符深度解析:从XSS防御到MyBatis实践

发布时间:2026/8/25 7:34:24
HTML转义符深度解析:从XSS防御到MyBatis实践 1. 项目概述为什么我们需要关注HTML转义符如果你写过HTML或者处理过从数据库、表单、API接口里吐出来的文本大概率遇到过这样的场景页面上本该显示一个“”小于号结果它把后面的标签给“吃掉”了导致页面布局错乱或者你想在页面上展示一段代码示例结果代码里的“”符号让整个实体解析都乱了套。这背后的问题就是我们今天要掰开揉碎了讲的——HTML转义字符。这个项目标题“【表格】html大于号转义符ampgt---小于号转义符amplt”虽然看起来像是一个简单的符号对照表但它指向的是Web开发、内容安全、数据展示中一个至关重要且容易被忽视的基础环节。它不仅仅是记住“”要写成“”那么简单。理解转义符是理解HTML如何区分“数据”和“指令”的关键是防止XSS跨站脚本攻击的第一道防线也是确保动态内容在各种环境下都能正确、安全渲染的基石。无论你是前端新手还是后端开发或者是经常需要编辑网页内容的运营、编辑搞懂转义符的原理和应用场景都能让你少踩很多坑。这篇文章我会从一个老开发的角度带你从“是什么”、“为什么”一直深入到“怎么用”和“怎么避坑”并结合那些热搜词里提到的实际场景比如MyBatis、邮件模板、数据导出把这块知识彻底讲透。2. 核心原理HTML转义符的本质与工作机制2.1 元字符与解析冲突转义的根本原因HTML文档本质上是一份由标记语言构成的文本文档。浏览器或HTML解析器在读取这份文档时会逐字符扫描并依据一套既定的规则来理解这些字符的含义。其中一些字符被赋予了特殊的语法功能我们称之为“元字符”或“保留字符”。最典型的五个元字符是(小于号)标记一个标签的开始例如div。(大于号)标记一个标签的结束例如/div。(与号)标记一个HTML实体或字符引用的开始例如copy;表示版权符号©。(双引号)在属性值周围用作定界符例如input typetext。(单引号)同样可作为属性值的定界符例如input typetext。冲突是如何产生的想象一下你想在网页的段落里显示一段数学不等式“5 10”。如果你直接写成p5 10/p解析器读到“5”后面的“”时会立刻进入“标签开始模式”它会试图寻找下一个“”来形成一个完整的标签名。它会把“ 10”当成一个名为“ 10”的标签这显然是非法的导致解析错误后面的内容可能无法正常显示或者整个DOM结构被打乱。转义的本质就是用一种没有特殊含义的、安全的字符序列来“代表”那些有特殊含义的元字符。当解析器遇到转义序列时它不会将其解释为语法指令而是将其转换回对应的普通字符并作为文本内容输出。这就完美解决了“把数据当代码执行”的冲突。2.2 转义序列的语法实体名称与实体编号HTML提供了两种主要方式来表示转义字符实体名称Entity Name和实体编号Entity Number。两者都以“”开头以“;”结尾。1. 实体名称可读性高格式name;例如lt;表示(less than)gt;表示(greater than)amp;表示(ampersand)quot;表示(quotation mark)apos;表示(apostrophe, 在HTML4中并非标准但在XHTML和XML中常用HTML5也支持)2. 实体编号兼容性最好格式#number;其中number可以是十进制或十六进制以x开头。 例如#60;或#x3C;表示(十进制60十六进制3C)#62;或#x3E;表示(十进制62十六进制3E)#38;或#x26;表示(十进制38十六进制26)#34;或#x22;表示(十进制34十六进制22)注意实体名称更容易记忆但并非所有字符都有对应的名称。实体编号则是万能的任何一个Unicode字符都可以用它来表示。在实际开发中对于那几个最常用的元字符, , , “通常使用实体名称对于特殊符号如©, €也多用名称copy;,euro;而对于一些非常用字符则可能需要查表使用编号。2.3 一个必须警惕的“陷阱”双重转义这是新手甚至一些有经验的开发者都容易栽跟头的地方。我们来看标题里一个有趣的细节“ampgt”。这看起来像是“gt;”被错误地转义了。gt;是正确的转义序列代表。如果这个字符串gt;本身需要作为纯文本显示比如你在教别人转义符你就需要对它本身的“”进行转义。所以应该写成amp;gt;。解析器处理amp;gt;的过程是先看到amp;将其转换为得到中间字符串gt;然后继续解析这个gt;再将其转换为。最终显示为。标题里写成了“ampgt”其中的分号是全角字符这是错误的正确应为半角;。但它揭示了一个关键场景当转义符本身作为数据内容时需要对它进行“双重转义”。这在动态生成HTML尤其是通过字符串拼接或模板渲染时是必须理清的层级关系。3. 核心场景深度解析从页面渲染到安全防御理解了基本原理我们来看看转义符在哪些具体场景下扮演着“守门员”的角色。结合热搜词这些场景非常普遍。3.1 场景一动态内容渲染与XSS防御这是转义符最重要的应用场景没有之一。任何来自用户输入、第三方API、数据库存储的内容在插入到HTML文档中时都必须经过适当的转义。反面教材致命错误假设有一个评论功能用户输入了如下内容scriptalert(你的站点被黑了);/script如果后端直接接收并存储前端又未经处理直接通过innerHTML或类似方式插入document.getElementById(comment).innerHTML userInput; // 灾难那么这段脚本就会被浏览器执行。这就是最典型的XSS跨站脚本攻击。正确做法在将不可信数据输出到HTML上下文时必须对其中包含的HTML元字符进行转义。前端渲染使用安全的API。绝对不要使用innerHTML来插入纯文本。应该使用textContent属性。// 安全 element.textContent userInput; // 浏览器会自动处理显示 会显示为字符“”而不是标签 // 危险 element.innerHTML userInput;如果确实需要渲染HTML比如富文本编辑器内容必须使用经过严格验证和过滤的库如DOMPurify进行“消毒”Sanitize而不仅仅是转义。后端模板渲染如Thymeleaf, FreeMarker, JSP现代模板引擎默认会自动进行HTML转义。!-- Thymeleaf 示例默认就是转义的 -- p th:text${userContent}/p !-- 安全 -- !-- 如果确实需要输出原始HTML慎用 -- p th:utext${trustedHtmlContent}/p !-- utext 表示不转义 --手动拼接字符串这是最危险但也有时不得已的方式。你必须手动调用转义函数function escapeHtml(text) { const map { : amp;, : lt;, : gt;, : quot;, : #x27; // 或 apos;但#x27;兼容性更好 }; return text.replace(/[]/g, function(m) { return map[m]; }); }实操心得在团队协作中一定要确立并遵守“默认转义”的原则。将“不转义”视为需要特别审批和审查的特例。很多安全漏洞都源于开发者在某个环节想当然地认为“这个数据是安全的”。3.2 场景二在HTML属性中的转义属性值同样需要转义尤其是当属性值来源于动态数据时。input typetext value用户输入的内容如果“用户输入的内容”里包含一个双引号就会提前闭合value属性导致后续内容变成无效的HTML属性甚至可能注入新的属性或事件。!-- 假设用户输入 onmouseoveralert(xss) -- !-- 未经转义的输出 -- input typetext value onmouseoveralert(xss)攻击者就成功注入了一个onmouseover事件。因此在将数据放入属性值时不仅要转义HTML字符还要特别注意引号。属性转义规则如果属性值用双引号包裹则需要转义其中的、、和。如果属性值用单引号包裹则需要转义其中的、、和。在实践中一个更简单粗暴且安全的做法是始终对、、、、进行转义无论它们出现在何处。很多转义函数如上面提到的escapeHtml就是这样做的。3.3 场景三特定技术栈中的转义问题热搜词里提到了“mybatis小于号转义”这是一个非常具体且常见的问题。MyBatis/XML中的转义在MyBatis的Mapper XML文件中SQL语句是写在XML标签里的。XML本身也有自己的保留字符最典型的就是、、。 如果你想在SQL中使用比较运算符直接写会与XML标签的起始符冲突。!-- 错误示例 -- select idfindUsers resultTypeUser SELECT * FROM user WHERE age 18 /selectMyBatis在解析这个XML文件时会报错因为它认为 18是一个不完整的标签。解决方案使用XML实体这是最标准的方法。select idfindUsers resultTypeUser SELECT * FROM user WHERE age lt; 18 /select使用CDATA区块CDATA内部的内容会被解析器忽略视为纯文本。select idfindUsers resultTypeUser ![CDATA[ SELECT * FROM user WHERE age 18 AND status 1 ]] /select这对于包含大量特殊字符如、、的复杂SQL片段非常方便。在SQL中避免使用对于简单的比较可以使用BETWEEN或函数但这不是通用方案。注意事项这里容易混淆层级。在MyBatis XML中转义为lt;是为了让XML解析器能正确理解文件结构。当这个SQL被MyBatis处理后发送给数据库时它已经是原始的SELECT * FROM user WHERE age 18了。这是XML层面的转义与最终HTML页面显示无关。如果这条查询结果要输出到网页那还需要经过前面讲的HTML转义流程。这是两个不同的上下文。3.4 场景四非HTML上下文中的“转义”转义的概念不限于HTML。你的数据可能在多个层级间流动每个层级都有自己的语法和保留字符。JavaScript字符串在JS字符串中如果包含换行、引号等需要用到反斜杠\进行转义。var str 用户说\你好世界\; // 或者 var str 用户说你好世界;当你需要将一段JSON数据内嵌到HTML的script标签中时情况更复杂它先要作为JS字符串被正确解析然后其内容可能还要被插入DOM。这时通常建议将JSON数据放在>let param name张三age20; let url /api/user? encodeURIComponent(param); // 错误整个字符串编码了 // 正确做法是对每个键值对分别编码 let url /api/user?name${encodeURIComponent(张三)}age${encodeURIComponent(20)};注意这里的被编码成了%26这是URL编码不是HTML实体。CSS上下文虽然较少见但在CSS的content属性或某些值中注入用户数据时也需要考虑转义防止CSS注入。核心原则在哪个上下文输出就使用哪个上下文的转义规则。永远不要假设一个转义函数能通用于所有场景。4. 实战指南如何系统性地处理转义知道了为什么和在哪里转义接下来我们看看具体怎么做。我将分前端、后端和工具三个层面来介绍。4.1 前端转义实践现代前端框架React, Vue, Angular在设计上就考虑了XSS防护大大降低了手动转义的需求但理解其原理依然关键。React默认转义在JSX中嵌入表达式{}React会自动对字符串进行转义。const userInput scriptalert(1)/script; function MyComponent() { return div{userInput}/div; // 安全会显示为文本不会执行脚本 }危险操作如果你确实需要插入原始HTML必须使用dangerouslySetInnerHTML属性并且必须确保内容是绝对可信或经过严格消毒的。const sanitizedHtml {__html: span可信的HTML/span}; return div dangerouslySetInnerHTML{sanitizedHtml} /;Vue文本插值使用双大括号{{ }}会进行HTML转义。template div{{ userInput }}/div !-- 安全 -- /template原始HTML使用v-html指令同样需要高度警惕。template div v-htmltrustedHtml/div /template原生JavaScript/DOM操作重申黄金法则使用textContent而非innerHTML来设置纯文本内容。// 安全 element.textContent untrustedData; // 危险除非数据已消毒 element.innerHTML untrustedData;4.2 后端转义实践后端是数据流转的枢纽在这里进行转义或标记数据状态至关重要。1. 模板引擎推荐绝大多数现代模板引擎默认开启自动转义Auto-escaping。这是最佳实践。Spring Boot Thymeleafth:text自动转义th:utext不转义。Python Django模板变量{{ variable }}自动转义使用safe过滤器或mark_safe函数可关闭转义。Node.js EJS/Pug/Handlebars通常需要检查配置但一般{{ }}会转义有专门的语法如%- %in EJS用于输出原始HTML。2. 手动转义函数当你不使用模板引擎或者需要在特定位置手动处理时需要使用语言提供的转义库。Java使用org.springframework.web.util.HtmlUtils.htmlEscape或 Apache Commons Lang 的StringEscapeUtils.escapeHtml4。Python使用html.escape()函数。import html safe_output html.escape(untrusted_input)JavaScript (Node.js)可以使用escape-html这个npm包或者自己实现前面提到的简单函数。3. 存储与传输的思考一个常见的争论是数据应该在存入数据库时转义还是在取出渲染时转义黄金法则在离“最终使用上下文”最近的地方转义。对于Web显示就是在输出到HTML时转义。为什么不在入库时转义数据复用性同一份数据可能用于HTML页面、移动端APIJSON、PDF导出等不同场景需要的转义方式不同。存储原始数据更灵活。避免双重转义如果在入库时转了显示时可能忘了导致显示lt;这样的字面文本。如果显示时又转一次就会变成amp;lt;更乱。可逆性转义通常是单向的。存储原始数据你永远知道数据的原貌。例外如果某个数据字段确定永远且仅用于HTML展示且系统架构简单在入库时由后端统一转义也是一种简化策略但需在文档中明确标注。4.3 工具与资源在线转义工具在遇到问题时可以快速使用在线工具验证。搜索“HTML Escape Tool”或“Online HTML Encoder/Decoder”有很多选择。浏览器开发者工具在Elements面板中你可以看到浏览器解析后的DOM。如果看到lt;被显示为说明转义正确如果看到字面量的lt;说明可能存在双重转义或未转义。OWASP Cheat SheetOWASP开放Web应用安全项目提供了详尽的XSS防护备忘单其中对转义有权威的指导。5. 常见问题与深度排坑指南在实际开发中关于转义的问题千奇百怪。我整理了几个最典型、最折磨人的案例和解决方案。5.1 问题一页面显示了“”而不是“”症状页面上原本应该显示小于号的地方直接显示了“”这五个字符。根本原因双重转义。数据被转义了两次。可能路径1数据库里存的是lt;已经转义过一次前端或模板渲染时又调用了一次转义函数将其中的转成了amp;于是变成了amp;lt;。浏览器解析时先将amp;还原为得到lt;但此时它已经过了HTML解析阶段被当作纯文本显示出来。可能路径2后端返回的JSON数据中字符串内容已经是转义后的lt;前端在通过innerHTML或类似方式插入时没有意识到它已被转义又进行了一次转义。排查步骤检查数据源直接查看后端API返回的原始JSON数据通过浏览器Network面板。看看值字段里是还是lt;。如果是lt;说明问题出在后端或数据库。检查后端逻辑追踪数据从数据库取出到API返回的整个链条。是否在某个环节如全局拦截器、序列化库的配置无意中进行了HTML转义检查前端渲染如果API返回的是检查前端是使用innerHTML还是textContent如果是innerHTML并且值中包含那么浏览器会将其解析为标签起始可能导致显示空白或布局错误而不是显示lt;。显示lt;恰恰说明前端可能对纯文本又做了一次转义。解决方案统一转义策略团队明确约定转义只在视图渲染层服务器端模板或前端框架的插值语法进行。API接口始终返回原始数据。净化数据流如果发现数据库或上游系统已经提供了转义后的数据且无法更改那么在下游渲染时需要先对数据进行反转义Unescape然后再根据当前上下文决定是否要重新转义。这是一个“脏数据”处理方案应尽量避免。// 一个简单的反转义函数注意不适用于所有情况谨慎使用 function unescapeHtml(text) { const map { amp;: , lt;: , gt;: , quot;: , #x27;: , #x2F;: / }; return text.replace(/(amp|lt|gt|quot|#x27|#x2F);/g, function(m) { return map[m]; }); }5.2 问题二JSON字符串中包含HTML标签导致解析错误症状后端返回的JSON在JSON.parse()时失败控制台报错“Unexpected token in JSON”。根本原因后端在生成JSON时没有对字符串值中的特殊JSON字符进行转义。在JSON中双引号、反斜杠\和控制字符如换行符都需要转义。如果字符串里包含了未转义的或换行就会破坏JSON的结构。更隐蔽的是如果字符串里包含了/script这样的字符序列当JSON被内嵌在HTML的script标签中时可能会被浏览器误认为是脚本块的结束。解决方案永远使用标准库序列化JSON不要手动拼接JSON字符串使用你所用语言的标准库如Java的Jackson/GsonPython的json.dumps()JavaScript的JSON.stringify()。这些库会自动处理所有必要的转义。# Python 正确示例 import json data {content: 这是一段包含\引号\和/script的文本} json_string json.dumps(data) # 库会自动转义引号和斜杠// Java Spring Boot 示例RestController 默认使用Jackson无需手动处理 GetMapping(/data) public MyData getData() { MyData data new MyData(); data.setContent(这是一段包含\引号\和/script的文本); return data; // Jackson会正确序列化 }前端安全接收使用fetch或axios等库获取JSON数据它们会自动调用JSON.parse()。避免使用eval()或手动拼接字符串到script标签中。5.3 问题三富文本编辑器如CKEditor、TinyMCE内容的处理症状用户通过富文本编辑器提交了带格式加粗、图片、链接的内容直接转义后所有HTML标签都变成纯文本了格式全丢。核心矛盾富文本内容本身就是HTML我们需要保留其中安全的HTML标签如b,img,a但过滤掉危险的标签和属性如script,iframe,onclick。解决方案消毒Sanitization而非简单转义。转义是“一刀切”把所有都变成lt;。消毒是“精细化过滤”只移除或中和危险的部分。实践方案后端消毒推荐在服务器端进行消毒更安全因为前端检查可以被绕过。Java可以使用OWASP Java HTML Sanitizer。Python可以使用bleach库。import bleach from bleach.sanitizer import ALLOWED_TAGS, ALLOWED_ATTRIBUTES # 定义允许的标签和属性 allowed_tags ALLOWED_TAGS [img, p, br, span] allowed_attrs {**ALLOWED_ATTRIBUTES, img: [src, alt, title], a: [href, title]} dirty_html user_input_from_editor clean_html bleach.clean(dirty_html, tagsallowed_tags, attributesallowed_attrs) # 然后将 clean_html 安全地存储或输出注意输出时通常不再进行HTML转义前端消毒需结合后端在提交前前端先做一次消毒提升用户体验但后端必须再做一次。JavaScript可以使用DOMPurify库。它是目前最受推崇的客户端HTML消毒库。const dirty userInput; const clean DOMPurify.sanitize(dirty, { ALLOWED_TAGS: [b, i, em, strong, a, p, img], ALLOWED_ATTR: [href, src, alt] }); // 然后将 clean 提交给后端后端仍需验证和二次消毒重要警告消毒规则的制定允许哪些标签、哪些属性是安全的关键。过于宽松会导致XSS风险过于严格会影响编辑器功能。这需要根据业务需求和安全团队共同制定白名单。5.4 问题四在JavaScript字符串中拼接HTML症状为了动态生成DOM在JavaScript中拼接HTML字符串然后赋值给innerHTML经常遇到引号不匹配、事件绑定失败或XSS漏洞。反面典型let userName userInput; // 假设用户输入了 img srcx onerroralert(1) let html div classgreeting你好 userName !/div; element.innerHTML html; // 灾难注入成功解决方案彻底放弃字符串拼接采用数据驱动的方式。使用现代前端框架React、Vue、Angular等均采用声明式模板和数据绑定从根本上避免了手动拼接HTML。使用模板字符串安全API如果必须用原生JS// 仍然不安全因为 userName 可能包含HTML // let html div classgreeting你好${userName}!/div; // 安全做法对动态部分进行转义 function escapeHtml(text) { /* ... 同上 ... */ } let safeUserName escapeHtml(userName); let html div classgreeting你好${safeUserName}!/div; element.innerHTML html;使用document.createElement和appendChild这是最安全但最繁琐的方式。let div document.createElement(div); div.className greeting; // 使用 textContent 添加文本内容自动安全 div.appendChild(document.createTextNode(你好)); div.appendChild(document.createTextNode(userName)); // textContent 特性保证安全 div.appendChild(document.createTextNode(!)); element.appendChild(div);5.5 问题排查速查表现象可能原因排查方向解决方案页面显示lt;,gt;等字符双重转义1. 检查API返回数据。2. 检查前端是否对已转义数据再次转义。1. 确保API返回原始数据。2. 渲染层统一做一次转义。页面布局错乱部分内容消失未转义的、被解析为标签1. 检查动态内容是否包含、。2. 检查是否错误使用了innerHTML。1. 对内容进行HTML转义。2. 使用textContent替代innerHTML。JSON解析失败JSON字符串中包含未转义的特殊字符1. 检查后端是否手动拼接了JSON。2. 检查数据中是否有未转义的引号、换行。使用标准JSON序列化库。富文本内容格式丢失对HTML内容进行了全文转义检查是否对富文本使用了普通转义函数。使用HTML消毒库进行白名单过滤。链接或图片属性被截断属性值中的引号未转义检查动态设置的href、src、title等属性值。使用安全的属性设置方法如setAttribute或框架绑定或确保属性值经过转义。在script标签内报错内联JSON或JS数据中包含/script序列检查内嵌在HTML中的JSON字符串。将/script拆分为/script或更好的做法通过>