本站的 HTML 格式化与压缩CSS 格式化与压缩XML 格式化都做同一件事:在浏览器本地接收文本,按对应语法重新排版,然后把结果显示出来。它们不会执行净化,也不声称输出可以直接插入生产页面、样式表或 XML 处理管线。

这句话听起来像免责声明,实际上是一个必须在设计阶段划清的工程边界。很多安全问题不是因为格式化器把字符改错了,而是调用方把“排版后的文本”误当成了“已经可以信任的文本”。格式化、解析、验证、净化解决的是四个相邻但不相同的问题:

操作 要回答的问题 通常的输出 它不保证什么
格式化 formatting 怎样让源码更易读或更紧凑? 重新排列空白、换行和缩进后的文本 不保证输入安全、语义正确或适合某个执行上下文
解析 parsing 这串字符在某种语法下能否变成结构? token、语法树、DOM 或错误 不自动替调用方制定安全政策
验证 validation 它是否符合我允许的语法、结构和业务规则? 通过/失败,或带诊断的结果 不会自动移除已不允许的节点或属性
净化 sanitization 如何移除或改写不应被消费的结构? 受约束的安全表示 仍依赖正确的上下文、策略和库配置

下面按 HTML、CSS、XML 展开。重点不是把四个词背下来,而是理解每一层究竟知道什么、又刻意不知道什么。


1. 格式化只改变表示,不改变允许集合

一个格式化器最常见的工作是读取 token,然后在 token 之间补空格、换行或缩进。它可能先解析成树,再把树序列化;也可能只做词法扫描。无论实现是哪一种,格式化通常都希望保持原有语义,而不是替用户改变语义。

这意味着,输入中原本存在的元素、属性、URL、注释、字符串和转义,往往会被原样带到输出中。下面这个输入可以被排成更漂亮的多行 HTML,但“格式化后”并不等于“其中的属性已经经过安全审查”:

<button class="save" title="保存" data-action="save">保存</button>

反过来,验证器会问“只允许 buttonclasstitledata-action 吗”,净化器则会依据策略删除或改写不允许的节点和属性。这些规则不是缩进算法能够推导出来的:一个产品是否允许表单、图片、链接、内联样式,取决于消费场景,而不是源码长什么样。

WHATWG HTML Living Standard 的解析章节描述的是浏览器如何把 HTML 字节流处理为树,包括错误恢复和插入模式。它不是“把任意 HTML 变成安全 HTML”的规范。浏览器为了兼容真实世界网页,会对不完整或不规范的标记进行错误恢复;这种恢复有助于渲染,却不能替应用建立一套允许列表。

可以把这条边界写成一句代码审查规则:只要函数名是 formatprettyPrintbeautifyminify,就不要从它的返回值推断安全属性,除非它的契约明确包含独立的策略验证和净化。


2. HTML:排得整齐,不会阻止 XSS

HTML 的危险性来自它被放进什么上下文,以及哪些结构会改变浏览器的行为。一个 HTML 格式化器通常能识别标签边界、属性边界和文本节点,但它并不知道这段结果最终会被:

  • 当作纯文本显示在日志或代码预览中;
  • 通过 innerHTML 放进文档;
  • 拼进某个属性、脚本字符串或模板;
  • 交给邮件客户端、富文本编辑器或另一个 HTML 解析器;
  • 保存后由不同的框架、不同的 CSP 和不同的权限上下文消费。

因此,HTML 格式化器不防 XSS。尤其不能因为输出多了换行、属性被统一加了引号,就认为危险结构消失了。需要在策略层单独处理的对象至少包括:

  1. 元素和属性允许列表:事件处理属性(例如 onclick 一类的 on* 属性)通常不应出现在用户可控 HTML 中;格式化器不会因为属性名看起来像普通属性就替你删除它。
  2. URL 属性hrefsrcactionformaction 等属性的安全性取决于协议、来源、重定向和消费上下文。安全策略通常需要解析 URL 后做协议和来源检查,而不是对字符串做一次大小写或空白替换。
  3. 嵌入与导航能力iframeobjectembed、表单和链接的风险取决于站点策略。把它们缩进得更整齐,不会撤销它们在浏览器中的能力。
  4. 上下文编码:把字符串放进 HTML 文本节点、双引号属性、JavaScript 字符串或 CSS 字符串,所需的编码规则不同。OWASP 将输出编码按上下文区分,正是因为“转义一下”不是通用安全操作。

OWASP Cross Site Scripting Prevention Cheat Sheet把安全输出编码、HTML 属性编码、URL 验证和安全 DOM API 分开讨论;HTML Sanitizer API 的 WHATWG 规范则描述了面向 HTML 片段的策略化净化接口。它们关注的是“哪些结构可以被消费”,而不是“这些结构怎样换行”。

一个防御性工作流应当是:先确定输入属于 HTML 还是普通文本,再决定允许的元素、属性和 URL 协议;净化后再把结果交给目标上下文;如果只是展示用户提交的源码,就使用文本节点或代码块显示,而不是把它当 HTML 解析。格式化可以发生在任意一步,但它不替代中间的安全策略。


3. HTML 解析器知道结构,仍然不知道业务策略

“先 parse,再 serialize”比正则替换更可靠,但它依旧不是净化。解析器可以告诉你某个属性落在某个元素上、某个标签被浏览器错误恢复成了什么结构,却不能回答:

  • 产品是否允许链接到外部站点?
  • 图片是否只能来自自有 CDN?
  • 用户内容能否包含表单?
  • 哪些 data-* 属性是业务字段,哪些不应被保留?
  • 这个文档是否将在带有脚本权限的页面中插入?

解析树让验证和净化变得可实现,但不自动完成验证和净化。尤其不要把“浏览器能够解析”理解成“应用应该接受”。HTML 规范中大量的错误恢复行为意味着,浏览器最终看到的树可能与人眼看到的源码边界不同;安全检查若只在字符串层做替换,可能检查了错误的表示。

这也是为什么“黑名单删除几个字符串”不是可靠的 HTML 净化方案:策略应当作用在解析后的节点、属性和 URL 上,并且使用经过审计、明确面向目标上下文的实现。格式化器没有必要承担这个职责,也不应该伪装成承担了这个职责。


4. CSS:空白是表面,解析上下文才是语义

CSS 格式化看起来比 HTML 更像排版:把声明拆成多行、统一冒号周围的空格、调整大括号缩进,或者删除注释和多余空白。但 CSS Syntax Module Level 3描述的 CSS 语法包含 token 化、转义、函数、URL token、块和错误恢复。格式化器必须尊重这些边界,却不因此获得“CSS 安全审查器”的能力。

CSS 的安全边界有几种常被混在一起的情况。

4.1 能被 CSS 解析,不等于允许出现在这个上下文

url()@import、字体加载、外部资源和自定义属性都可能把声明与网络请求、资源来源或后续拼接联系起来。一个格式化器可以把下面的合法 CSS 排成多行:

.card {
  background-image: url("/images/card.svg");
  --card-title: "示例";
  color: rgb(32 32 32);
}

但它不会知道当前页面是否允许外部样式表、这个资源是否来自可信源、SVG 是否经过单独处理,也不会替你验证自定义属性最终被展开到哪里。格式化器只看到 token;安全策略还需要知道文档、来源、CSP、资源加载和应用拼接关系。

4.2 CSS 解析错误有恢复规则,字符串替换看不到恢复后的结构

CSS 不是一组可以随意用正则删除的 property: value 文本。注释、字符串、转义、括号和块的边界会影响后续 token。一个看似“删除危险片段”的替换可能改变 token 边界,导致浏览器和服务端工具对剩余文本得出不同结论。更稳妥的做法是使用同一类语法解析器,在明确的允许集合上验证声明,再按目标平台重新序列化。

这不意味着每个 CSS 工具都需要实现完整的安全策略。相反,职责应当分开:格式化器保留语义并改善可读性;构建系统或策略检查器验证允许的 at-rule、属性、URL 和来源;浏览器侧再用 CSP 等纵深防御限制最终能力。若应用要接受用户 CSS,还要明确它是在隔离文档、受限 iframe、Shadow DOM,还是主文档样式上下文中使用——同一段 CSS 的风险不由缩进决定。

4.3 “浏览器能显示”也不是验证结论

CSS 规范定义了用户代理如何解析和恢复错误,但应用可能还需要限制版本、属性集合、值域、资源协议或布局能力。验证应针对业务契约,例如“只允许颜色和有限的排版属性”,而不是简单地问“浏览器有没有报错”。W3C CSS Syntax给的是语法基础;允许集合、资源策略和部署隔离仍然属于应用安全设计。


5. XML:格式化绝不等于禁用外部实体

XML 这一点最容易造成危险的错觉:文档有清晰的树、标签和属性,格式化结果也很直观,于是有人把“能漂亮地展开 XML”理解成“解析器已经安全”。实际上,XML 规范定义了实体、DTD 和外部标识符等机制;是否读取外部资源、是否展开实体、是否允许 DTD,取决于具体 XML 解析器及其配置。

W3C XML 1.0规定了实体声明和外部标识符等语法能力,但规范存在不等于每个运行环境都必须启用每一种外部访问能力。反过来,应用也不能因为某个浏览器示例没有发起网络请求,就把同样的 XML 交给服务端解析器而不检查配置。

服务端 XML 解析的防御重点通常包括:

  • 禁用 DTD,或至少禁用外部实体和外部参数实体;
  • 禁止通过实体解析器访问网络、文件系统或其他外部资源;
  • 对输入大小、嵌套深度和实体展开设置限制,防止资源耗尽;
  • 使用库的安全模式和明确的解析器配置,而不是依赖版本默认值;
  • 在解析前后分别验证来源、结构、大小和业务字段。

OWASP XML External Entity Prevention Cheat Sheet按语言和库列出了禁用 DTD、外部实体和外部资源访问的防御方向。这里的关键词是“解析器配置”,不是“格式化器选项”。一个 XML 美化器即使能把节点展开成树,也不代表它改变了另一套服务端解析器的实体策略。

因此,“格式化 XML”与“安全解析 XML”至少是两条链:

原始文本 →(受配置约束的解析)→ 结构 →(业务验证)→ 允许的模型 →(序列化/格式化)→ 输出文本

如果输入只需要展示,应该把它作为文本展示,或在明确的隔离环境中使用适合展示的解析路径;如果输入要进入服务端 XML 业务链,则应在服务端使用安全配置重新解析。不要因为它已经在浏览器里格式化过,就跳过服务端的安全配置。


6. 浏览器 DOMParser 的能力边界

浏览器的 DOMParser可以把字符串解析为 HTML 或 XML Document。这对预览、结构检查和编辑器功能很有用,但它仍然是一个解析 API,不是通用的 HTML/XML 净化 API。

HTML 解析得到的文档通常处于适合离线处理的惰性状态:其中的脚本不会因为“被解析”就执行。然而,这个事实不能被扩大解释为“内容安全”。一旦应用把节点复制、序列化,或通过某种方式插回有脚本能力的文档,风险就重新取决于插入 API、节点类型、属性、URL 和页面策略。解析阶段没有替应用完成允许列表,也没有替后续插入动作做上下文编码。

XML 模式同样只说明“按 XML 语法解析并得到文档”,不说明服务端库会使用同样的 DTD、实体和外部资源策略。浏览器实现通常会限制或不提供服务端 XML 库那类可配置的外部实体访问能力,但这不是跨平台的安全契约。DOMParser 的结果不应作为“已禁用 XXE”的证明,更不能作为服务端解析器配置的替代品。

实际判断时要问三个问题:

  1. 谁解析? 是浏览器内置 HTML/XML 解析器,还是 Java、.NET、Python、Go 等服务端库?
  2. 解析结果去哪? 只是文本预览、进入 DOM、保存到数据库,还是继续喂给另一个解析器?
  3. 安全策略在哪里执行? 元素/属性允许列表、URL 方案、资源访问、DTD 和实体限制分别由哪一层负责?

只回答“它能不能 parse”是不够的;安全设计要回答“它被允许解析成什么,以及解析结果能被谁消费”。


7. 本站工具的明确契约:本地格式化,不执行净化

本站三个相关工具的定位很窄,也因此更容易保持诚实:它们在浏览器本地处理输入,不把内容上传到服务器,不把输入当作本站页面的一部分执行。输出是格式化后的源码,方便阅读、比较和复制;输出不是安全审计报告,也不是经过策略净化的文档。

这条契约带来几个实际结果:

  • HTML 工具不会替用户删除事件属性、筛选元素或验证 URL;
  • CSS 工具不会判断某个资源是否符合部署策略,也不会把 CSS 变成隔离样式;
  • XML 工具不会为任意服务端 XML 库关闭 DTD 或外部实体;
  • 工具不会把“解析成功”标成“可以安全上线”;
  • 用户可以把结果当源码阅读,但把结果交给生产渲染器、模板、样式管线或 XML 服务前,仍必须执行对应上下文的验证和安全处理。

这和本站 SQL 词法分词器与代码格式化的边界是一致的:分词器能保护字符串字面量不被格式化规则改写,但 SQL 格式化器不是数据库权限系统。也和 Markdown 解析器与 KaTeX 数学排版的分工相似:解析器负责把结构识别出来,排版器负责把公式排出来;“识别”和“允许它被怎样消费”仍然是两个阶段。


8. 一张交付前检查表

当团队要把一个格式化结果交给下游时,可以用这张短表阻止概念越界:

  • 目标是什么? 只是可读性、压缩体积,还是需要拒绝不符合策略的输入?
  • 输入是什么? HTML、CSS 和 XML 的语法与错误恢复不同,不要用通用正则代替解析器。
  • 输出在哪里消费? 文本节点、HTML、属性、脚本、样式表、URL 和服务端 XML 解析器需要不同的安全规则。
  • 是否有独立验证? 记录允许的元素、属性、声明、协议、来源、大小和嵌套限制。
  • 是否有独立净化? 如果需要移除结构,使用目标上下文的成熟实现,并在升级库后重新测试。
  • 解析器是否安全配置? 尤其检查 XML 的 DTD、外部实体、外部资源访问和资源限制。
  • 是否有纵深防御? CSP、隔离 iframe、权限边界、网络出口限制和服务端重新验证不能由 formatter 代替。

最后可以把四个概念压缩成一句话:格式化改变“怎么写”,解析决定“它是什么”,验证决定“是否允许”,净化改变“留下什么”;它们不是同义词。 一个工具越诚实地只做其中一件事,调用方越不容易把排版结果误当成安全边界。

参考资料