PHP 8.6 的 trim() 开始默认剥掉换页符 \f:签名串、CSV 和粘贴文本都会变
PHP 8.6 将换页符 \f(0x0C)加入了 trim()、ltrim()、rtrim() 的默认字符集。默认掩码的其他字符没有变化;只要显式传入第二个参数 $characters,行为就与旧版完全一致。这会破坏向后兼容性,影响依赖 \f 被保留的代码。
变更点
旧版默认掩码是空格、\t、\n、\r、\0、\x0B,8.6 在其中加入了 \x0C。这个改动对于“清洗用户输入”类代码基本没有感知,但在以下三个地方会把原本属于数据的字节变成“需要删掉的空白”。
先复现
// PHP 8.5
echo bin2hex(trim("abc\x0c")), PHP_EOL; // 6162630c
// PHP 8.6
echo bin2hex(trim("abc\x0c")), PHP_EOL; // 616263
三个函数都受影响,并且显式传字符集就能恢复旧行为:
var_dump(bin2hex(ltrim("\x0cabc"))); // 8.5: 0c616263 8.6: 616263
var_dump(bin2hex(rtrim("abc\x0c", " \t\n\r\0\x0B"))); // 两个版本都是 6162630c
后果一:验签、加签前 trim 过的字段
常见写法是先清洗字段,再计算 HMAC 或拼接签名串:
$canonical = trim($row['payload']);
$sign = hash_hmac('sha256', $canonical, $key);
历史数据里的 \f 来源很杂:打印子系统分页、Excel 导出、PDF 文本提取都会带出来。旧版 trim() 把 \f 留在串里,这个值既入了库,也参与了当时的签名。升级到 8.6 后,同样的代码从库里读出字段再 trim,\f 被吃掉,算出的摘要和库中存的签名对不上。表现为一批老数据突然验签失败,而新写入的数据正常,这容易让人误判为密钥问题。
后果二:CSV、TSV 解析
手写解析器通常用 rtrim($line, "\r\n") 去掉行尾换行,字段再用 trim() 清洗。旧版会保留字段尾部的 \f,8.6 则直接裁掉,导致同一份历史文件在升级前后解析出的字段值不同。如果下游还要对整行或整字段做哈希、去重、比对,结果就会全部改变。
后果三:粘贴进表单的多行文本
从 PDF 或终端里复制一段多行内容粘贴到 textarea,尾部可能挂着一个 \f。旧版原样入库,8.6 会静默移除,用户和日志都看不到任何提示。对普通备注无所谓,但对“粘贴原文留证”类字段,内容已被改动却不自知。
如何找出存量里的 \f
文件侧:
grep -rlP '\x0c' ./exports
grep -rlP -a '\x0c' ./exports # 含二进制或大文件时加 -a
数据库侧(MySQL 用 CHAR(12)):
SELECT id FROM orders WHERE INSTR(payload, CHAR(12)) > 0;
先统计命中量,再决定是清洗数据,还是保留字节并在代码里显式传字符集。
跨版本一致的写法
让字符集显式出现,不要依赖默认值:
const TRIM_LEGACY = " \t\n\r\0\x0B"; // 8.5 默认,保留 \f
const TRIM_860 = " \t\n\r\0\x0B\x0C"; // 8.6 默认,吃掉 \f
$canonical = trim($row['payload'], TRIM_LEGACY);
凡是参与签名、哈希、去重、比对的字符串,把掩码定义成常量统一引用,在 8.5 和 8.6 上跑出来的字节就一致。
8.6 标准库的其他改动
pack()、unpack()的整数格式码s S l L q Q支持 Perl 风格的<(小端)和>(大端)修饰符,不再需要手写位运算;用在已有固定字节序的v n V N P J或非数字格式码上会抛ValueError。- 同一组修饰符扩展到浮点:
f<等价g、f>等价G、d<等价e、d>等价E;用在g G e E上抛ValueError。 - 新增
mysqli_quote_string()/mysqli::quote_string():转义的同时按连接 SQL 模式选择引号包裹。real_escape_string()的输出在双引号内配合NO_BACKSLASH_ESCAPES是不安全的,这个函数处理该场景;预处理语句仍是推荐路径。 - 限制
php://filter过滤器链长度,针对用长convert.iconv.*链构造任意内容的 LFI 攻击。8.6 中超过 16 个过滤器会发E_DEPRECATED,除非设置了流上下文选项filter.max_filter_count;设置后超限会以警告方式打开失败。RFC 计划在 8.7 把默认行为改为报错。 - 会话默认值收紧:
session.use_strict_mode0→1,session.cookie_httponly0→1,session.cookie_samesite由未设置改为Lax。 - 弃用共 28 项,包括在
finally块中return、is_double()/is_integer()/is_long()/doubleval()、spl_object_hash()、strcoll()与SORT_LOCALE_STRING、metaphone()、spl_classes()、SplFileObject的 CSV 方法,以及向array_walk()和http_build_query()传对象。