编程 PHP 8.6 的 trim() 开始默认剥掉换页符 \f:签名串、CSV 和粘贴文本都会变

2026-11-06 21:03:00

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_mode 0→1,session.cookie_httponly 0→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() 传对象。
复制全文 生成海报 PHP 字符串处理 签名 升级迁移

推荐文章

程序员茄子在线接单