多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

PHP htmlspecialchars()函数用法与实例讲解

PHP htmlspecialchars()函数用法与实例讲解 前言htmlspecialchars()是 PHP 里最常用的输出转义函数它把字符串里几个对 HTML 语法有特殊含义的字符替换成对应的实体entity让数据可以安全地放进 HTML 页面里显示——用户输入的尖括号会显示成尖括号本身而不会被浏览器当成标签解析。它是防跨站脚本XSSCross-Site Scripting的第一道也是最基本的一道防线。关于它有几个流传很广但不准确的说法说法一「htmlspecialchars()会转义所有有危险的字符。」它只转义 5 个字符、、、、其中后两个是否转换取决于标志位。放到 URL、JavaScript、CSS 上下文里它完全不管用那些上下文各有各的编码方式。说法二「用了它就可以直接输出数据库里的内容。」正确用法是存原始数据、输出时转义而不是存的时候就转义好。存转义后的数据会让同一份数据在非 HTML 场景导出 CSV、调接口里全是实体符号。说法三「默认参数就够安全了。」默认参数在PHP 8.1 之前是ENT_COMPAT它不转换单引号。如果你的 HTML 属性用单引号包裹那个版本上的默认调用是有洞的。本文把签名、转换表、编码参数、以及三个上下文的正确写法逐一讲清楚所有描述以 PHP 官方手册为准。一、签名与默认参数官方手册给出的签名是htmlspecialchars(string $string,int $flags ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401,?string $encoding null,bool $double_encode true): string四个参数的含义参数默认值作用$string无待转义的字符串$flagsENT_QUOTES \ENT_SUBSTITUTE \$encodingnull字符编码省略时取配置项default_charset的值$double_encodetrue是否对已有的实体再做一次编码这里有一个必须记住的版本差异上面这个默认值只在PHP 8.1.0 及以上成立。PHP 8.1 的迁移文档写明htmlspecialchars()、htmlentities()、htmlspecialchars_decode()、html_entity_decode()、get_html_translation_table()这几个函数默认标志从ENT_COMPAT改成了ENT_QUOTES | ENT_SUBSTITUTE。影响是两条单引号现在会被转义成#039;以前什么都不做非法的 UTF-8 字节序列会被替换成 Unicode 替换字符 UFFFD以前会让函数返回空字符串。所以在 PHP 8.1 以下的版本里推荐显式把标志写全不要依赖默认值。本文所有示例都采用显式写法的原因就在这里。二、精确的转换表htmlspecialchars()只做五组替换逐字符列出如下以ENT_QUOTES | ENT_HTML401为例输入字符输出实体备注和号amp;恒定转换小于号lt;恒定转换大于号gt;恒定转换双引号quot;设了ENT_NOQUOTES时不转换单引号#039;仅在设了ENT_QUOTES时转换单引号这一格还有个例外文档类型不同实体写法也不同。按手册的说明ENT_HTML401下输出#039;数字实体而在ENT_XML1、ENT_XHTML、ENT_HTML5下输出apos;命名实体。两者的显示效果一样但如果你后续要做字符串比对或走 XML 解析器这个差别会体现出来。完整的标志位常量常量含义ENT_COMPAT转换双引号保留单引号ENT_QUOTES双引号和单引号都转换ENT_NOQUOTES两种引号都不转换ENT_IGNORE丢弃非法的代码单元序列而不是返回空字符串手册明确不推荐有安全隐患ENT_SUBSTITUTE非法序列替换成 UFFFDUTF-8 下或#xFFFD;而不是返回空字符串ENT_DISALLOWED把对当前文档类型非法的码点替换成 UFFFDENT_HTML401/ENT_XML1/ENT_XHTML/ENT_HTML5按对应文档类型处理这些常量可以用按位或自由组合例如ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5。一个完整示例?php // 适用于 PHP 8.0标志位显式书写兼容 8.1 之前的默认值差异$input Tom Jerry bquoted/b and \single\;echo htmlspecialchars($input, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, UTF-8), PHP_EOL;输出是Tom amp; Jerry lt;bgt;quot;quotedquot;lt;/bgt; and #039;single#039;原本会被浏览器当作加粗标签处理的部分现在会原样显示成文本。三、编码参数与「返回空字符串」的陷阱第三个参数$encoding省略时会取配置项default_charset的值PHP 发行包里的php.ini-development中该值默认是UTF-8。手册对这一段的建议很明确虽然它是可选参数但强烈建议显式写出正确的编码因为default_charset可能被改错。如果不显式指定编码而输入字符串的实际编码又和配置不符麻烦就来了。手册对ENT_SUBSTITUTE的描述从反面点破了这个问题——如果不设这个标志遇到非法的代码单元序列时函数会返回空字符串。也就是说?php // 适用于 PHP 8.0$bad \xC3\x28; // 一段不合法的 UTF-8 字节序列var_dump(htmlspecialchars($bad, ENT_QUOTES | ENT_HTML401, UTF-8));// 没有 ENT_SUBSTITUTE 时返回 string(0) 内容被静默吞掉var_dump(htmlspecialchars($bad, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, UTF-8));// 返回被替换后的字符串长度不为 0「整个页面某块内容突然空白」这类诡异现象很多时候就是这里来的数据里混入了一个非法字节函数返回空串模板照常渲染于是内容凭空消失还看不到报错。养成在标志位里带上ENT_SUBSTITUTE的习惯可以让问题变成可见的替换字符而不是静默丢失。还有一点容易被忽略htmlspecialchars()的转义范围是「HTML 语法层面」的被转义成amp;只是为了保护它自己不被当成实体开头。如果输入里已经存在合法的实体例如数据里本来就存着amp;这种文本默认的$double_encode true会把它再次编码成amp;amp;页面上就会显示成amp;这个字面文本。这时需要根据数据来源决定是否传false?php // 适用于 PHP 8.0$s ATamp;T 公司;echo htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, UTF-8), PHP_EOL;// ATamp;amp;T 公司echo htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, UTF-8, false), PHP_EOL;// ATamp;T 公司$double_encode false只应该在「数据里本来就含合法实体」时使用如果数据是原始用户输入保持默认的true。四、实战三个上下文的正确做法htmlspecialchars()只管 HTML 文本和属性这两个上下文。把数据放到别的位置时要用对应的编码函数。上下文一HTML 文本内容?php // 适用于 PHP 8.0function h(?string $s): string{return htmlspecialchars((string) $s, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, UTF-8);}$comment 用户输入b加粗/b;echo 内容, h($comment), PHP_EOL;把这个h()当成模板层的唯一出口所有变量都从它过一遍是防止遗漏的最简单办法。示例里用(string)显式转换避免了给参数传null触发弃用提示PHP 8.1 起给内部函数的非可空参数传null会产生弃用提示。上下文二HTML 属性属性值的转义对引号处理有额外要求属性值一定要用引号包起来且转义时带上ENT_QUOTES。如果属性用双引号包裹而只做了ENT_COMPAT的处理单引号不会被转换——虽然在这个属性里单引号无法闭合双引号属性但只要你换成单引号包裹属性就会直接被闭合、进而注入新属性。用ENT_QUOTES一次性解决两边。?php // 适用于 PHP 8.0$url /search?qtest;$name He said hi left; // 值里含双引号与和号echo a href, h($url), title, h($name), 链接/a, PHP_EOL;属性里出现没有转义的双引号时浏览器会认为属性值已经结束后面的内容被当作新的属性解析——这正是属性上下文必须带ENT_QUOTES的原因。同时注意URL 放进href前还要单独做 URL 层面的校验。htmlspecialchars()只保证不能从这个属性里逃出去不保证javascript:这类协议安全。协议白名单应该单独做。上下文三URL 与 JavaScript 参数不要用 htmlspecialchars?php // 适用于 PHP 8.0$keyword 搜索 关键词 more;// URL 参数用 rawurlencode$url /search?q . rawurlencode($keyword);echo $url, PHP_EOL;// 多参数用 http_build_query 更省事echo http_build_query([q $keyword, page 2]), PHP_EOL;把数据嵌进 JavaScript 时应该用json_encode()而不是htmlspecialchars()?php // 适用于 PHP 8.0$data [name Tom Jerry, tags [a, b]];echo scriptconst payload ,json_encode($data, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT),;/script, PHP_EOL;JSON_HEX_TAG等几个标志会把尖括号、和号、引号转义成 Unicode 转义序列避免数据提前闭合脚本块。不过更稳妥的工程做法是不要把动态数据直接写进脚本块而是放在一个data-属性或独立接口里由前端去取。附反向操作需要把实体还原成字符时用htmlspecialchars_decode()只还原上面那 5 个或者html_entity_decode()还原所有命名实体。想知道具体有哪些字符会被转换可以用get_html_translation_table()拿到转换表。?php // 适用于 PHP 8.0$html Tom amp; Jerry;var_dump(htmlspecialchars_decode($html, ENT_QUOTES));var_dump(html_entity_decode($html, ENT_QUOTES | ENT_HTML401, UTF-8));常见坑点❌ 只写htmlspecialchars($s)在 PHP 8.1 之前的版本上依赖默认标志✅ PHP 8.1 之前默认是ENT_COMPAT不转单引号、非法序列返回空串。显式写全ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401并带上UTF-8。❌ 页面某块内容莫名空白找不到报错✅ 很可能是数据里有非法编码的字节而标志位里没有ENT_SUBSTITUTE函数返回了空字符串。加上这个标志能看到替换字符问题立刻可见。❌ 在入库前先把用户输入转义输出时直接用✅ 转义应该发生在输出时。存原始数据输出到 HTML 时转义否则同一份数据导出 CSV、调 JSON 接口时全是amp;这类实体还会和多一次转义叠加。❌ 用htmlspecialchars()处理要放进 URL 参数的值✅ HTML 转义对 URL 语义无效。URL 参数用rawurlencode()多个参数用http_build_query()。❌ 用htmlspecialchars()处理要嵌进脚本块的数据✅ 用json_encode()配合JSON_HEX_TAG等标志更好的做法是把数据放到data-属性或独立接口里。❌ 属性值不写引号或写引号但不用ENT_QUOTES✅ 属性值始终用引号包裹转义时带ENT_QUOTES。省掉引号的属性是注入的常见入口别为了少打几个字符留下口子。❌ 对已经含实体的数据再次转义页面上显示出amp;这串字面文本✅ 确认数据来源原始用户输入保持$double_encode true数据本身已含合法实体时才传false。❌ 以为转义了就万事大吉把javascript:协议、data:协议的链接直接放进href✅htmlspecialchars()只防「从 HTML 语法里逃出去」不判断协议安全性。URL 类属性还要加协议白名单只允许http:、https:等必要时用filter_var($url, FILTER_VALIDATE_URL)校验。总结项目说明签名htmlspecialchars(string $string, int $flags ENT_QUOTES \转换字符、、、、共 5 个与htmlentities()的「全部命名实体」不同单引号实体ENT_HTML401下是#039;ENT_XML1/ENT_XHTML/ENT_HTML5下是apos;默认标志变更PHP 8.1.0 起默认从ENT_COMPAT变为 ENT_QUOTES \编码参数省略时取default_charset建议始终显式传UTF-8适用上下文HTML 文本内容、HTML 属性值不适用上下文URL用rawurlencode()、JavaScript用json_encode()、SQL用参数化查询反向操作htmlspecialchars_decode()、html_entity_decode()转换表查询get_html_translation_table()htmlspecialchars()的正确用法可以压缩成三句话输出时转义不是入库时转义标志位写全别依赖默认值只在 HTML 的文本和属性上下文用其他上下文各用各的编码函数。它是防 XSS 的必要条件而不是充分条件——它挡住了「用数据拼出标签」这条路但协议白名单、富文本清洗、内容安全策略CSP这些属于另外的题目。把这几层分清楚这个函数就算真正用明白了。
返回列表