Menu

PHP htmlspecialchars():转义 HTML,防止 XSS

htmlspecialchars($text) 会把 HTML 中有特殊含义的五个字符(& < > " ')转换成实体,让用户输入显示为文本,而不会被当作标记执行。本文讲解 ENT_QUOTES、转义属性、重复编码、htmlentities 和 strip_tags。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

htmlspecialchars($text) 把 &、\u003C、\u003E、" 和 ' 转换成 &amp;、&lt;、&gt;、&quot; 和 &#039;。对输出到页面中的每一段用户输入都调用它,输入就会显示为文本,而不会被当作 HTML 解析。

这段代码把同一条评论输出两次,一次原样输出,一次转义后输出。运行它,在“页面”标签页中比较这两行,然后在表单里输入你自己的 HTML,例如 <h1>big</h1> 或 <img src=x>,再点 Show。

在原样输出的那一行,浏览器会执行这些标签:加粗的词显示为粗体;如果访问者输入 <script>,他的脚本就会在其他每个读者的浏览器中运行。这种攻击叫作跨站脚本攻击(XSS)。在转义后的那一行,同样的字符以 &lt;b&gt; 的形式到达,浏览器把它们画成文本。切换到“输出”标签页,就能看到 PHP 实际输出的实体。

htmlspecialchars 会转换哪些字符

只有五个字符,别的都不转换。字母、带重音的字符和 emoji 都原样通过。

& 也在列表中,因为每个实体都以它开头:如果不转换它,一条提到 &lt; 的评论就会显示成 \u003C。

不只转义文本,也要转义属性

属性中的用户输入同样需要转义。如果不转义,值里的引号会闭合属性,输入的其余部分就变成了新的属性。下面这个“名字”偷偷塞进了一个 style 属性;运行它,看看这两个输入框。

在不安全的输入框中,浏览器看到的是 value="Ada",后面跟着一个新的 style 属性,所以输入框变成红色,并且只显示 Ada。攻击者会在那里写 onfocus="..." 而不是 style,他们的代码就会运行。在安全的输入框中,每个 " 都变成了 &quot;,所以整个字符串都留在 value 里,并按输入的样子显示。

从 PHP 8.1 开始,默认标志是 ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401,所以单引号也会被转义,用 '...' 写的属性也是安全的。旧代码常常手动传入 ENT_QUOTES,在 PHP 7 及更早的版本中这是必需的:

模板中使用的简短辅助函数

在模板里写几十次 htmlspecialchars($x, ENT_QUOTES, 'UTF-8') 太啰嗦,所以大多数项目会把它包装成一个单字母函数。Twig 和 Blade 这样的模板引擎会对每个 {{ $var }} 自动做同样的事。

?string 类型和 ?? '' 很重要:从 PHP 8.1 开始,把 null 传给 htmlspecialchars() 已被弃用,你的 PHP 会为每个没有简介的用户输出一条弃用提示。

重复编码和 htmlspecialchars_decode

如果一个值被转义了两次,读者就会看到实体:& 第一次变成 &amp;,第二次变成 &amp;amp;,浏览器显示为 &amp;。这通常说明值在保存时转义了一次,输出时又转义了一次。传入 double_encode: false 可以保留已有的实体不动,用 htmlspecialchars_decode() 则可以还原。

真正的解决办法是保存原始文本,只在输出时转义。double_encode: false 适用于已经包含不是你自己创建的实体的文本,比如导入的订阅源。

htmlspecialchars、htmlentities 和 strip_tags 的区别

这三个函数经常被混淆。下面的代码对同一个输入分别运行它们,并显示每个函数让 PHP 输出了什么,以及浏览器最终显示成什么:

  • htmlspecialchars() 转义 HTML 的五个特殊字符。输出到 HTML 中的任何文本都用它。
  • htmlentities() 还会把 é 变成 &eacute;。在页面还不是 UTF-8 的年代它很有用;如今它只会让源代码更难读。
  • strip_tags() 删除标签,保留其中的文本。它用于把 HTML 转成纯文本(邮件预览、meta 描述),而不是用于安全防护:最后一行显示,被允许的 <b> 保留了它的 onclick,而放在属性里的文本则完全不受影响。

htmlspecialchars 不够用的地方

htmlspecialchars() 是转义 HTML 文本和带引号属性的正确方法。页面中的其他位置有各自的规则:

  • 在 URL 中,用 http_build_query() 或 urlencode() 对值进行编码;然后 htmlspecialchars() 让参数之间的 & 成为合法的 HTML。
  • 在 JavaScript 中,json_encode() 生成合法的 JS 值,JSON_HEX_TAG 会把 \u003C 和 \u003E 转成 \u003C 和 \u003E,这样数据中的 </script> 就无法闭合 script 标签。
  • 不检查协议就绝不要把用户输入输出到 href 中:htmlspecialchars('javascript:alert(1)') 的结果不变,点击时仍然会运行。只接受 http 和 https 的 URL,做法见 filter_var 页面。

综合运用这些知识的表单处理,见 PHP 表单。

常见问题

PHP 中的 htmlspecialchars 有什么作用?

它把 &、<、>、" 和 ' 替换成 &amp;、&lt;、&gt;、&quot; 和 &#039;。这样浏览器会直接显示这些字符,而不是把它们当作 HTML 解析,所以在表单里输入的 <script> 会显示为文本,永远不会运行。

htmlspecialchars 和 htmlentities 有什么区别?

htmlspecialchars() 只转换 HTML 中的五个特殊字符。htmlentities() 还会转换所有具有命名实体的字符,所以 café 会变成 caf&eacute;。在 UTF-8 页面中两者同样安全,而 htmlspecialchars() 的输出更易读,所以通常选它。

PHP 8 中还需要 ENT_QUOTES 吗?

从安全角度看不需要:从 PHP 8.1 开始,默认标志是 ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401,单引号也会被转义。很多代码库仍然显式传入 ENT_QUOTES, 'UTF-8',这样调用在旧版本上的行为保持一致,读代码的人也一目了然。

htmlspecialchars 应该用在输入时还是输出时?

用在输出时。保存并验证原始值,在把它输出到 HTML 的那一刻再转义。在输入时转义会把 &lt; 存进数据库,破坏长度计算和搜索,还会导致 &amp;lt; 这样的重复转义。

strip_tags 足以防止 XSS 吗?

不够。strip_tags() 会删除标签,但用它的允许标签参数保留下来的标签会连同属性一起保留,所以 <b onclick="..."> 能原样通过;而且它对放在属性里的文本完全不起作用。输出用户输入时,请使用 htmlspecialchars()。

Coddy 编程语言插图

和 Coddy 一起学编程

开始