HOWTO · PHP

在 PHP 中清理输入

在今天的帖子中,我们将学习如何在将输入数据传递到数据库之前清理 PHP 中的输入数据。

本页内容

没有一种通用步骤能让所有不可信值都变得安全。应先判断值代表什么,再按相应要求验证;只有在应用有明确规则时才进行规范化;显示时针对具体上下文进行编码;发送到数据库时使用参数化查询。这些操作解决的问题各不相同:验证会接受或拒绝一个值,清理会转换它,输出编码则会针对特定上下文安全地表示字符。单独使用其中任何一种,都不能保证数据完整性,也不能保护该值的所有用途。

PHP 输入清理能做什么、不能做什么

处理输入要从字段的约定开始。年龄、电子邮件地址、显示名称和搜索词各有不同的有效形式。验证会检查值是否符合该约定;规范化则是有意且有明确依据的转换,例如去除首尾空白。清理器可能删除或转换字符,但这可能悄悄改变用户提交的内容,也不能让结果适用于之后的所有用途。

如果应用需要保留原始值,应将它与规范化或编码后的表示分开保存。不要仅仅因为某些字符看起来特别就将其删除:姓名和其他合法文本可能包含标点或非 ASCII 字符。也要根据实际用途选择处理方式。HTML 输出需要 HTML 编码,SQL 值则需要查询参数。这些保护措施不能互相替代。

显示用户输入时转义 HTML

将文本插入 HTML 文本节点或带引号的属性时,应对其进行编码。对于普通 HTML 输出,htmlspecialchars() 会转换具有 HTML 特殊含义的字符,包括与号、尖括号,以及在使用相应标志时的引号。htmlentities() 还会编码具有命名 HTML 实体的字符。这两个函数都不是通用的 HTML 清理器:针对文本节点编码一个值,并不意味着可以安全地将它插入 JavaScript、CSS、URL 或原始 HTML 标记中。应针对确切的输出上下文使用合适的编码器和规则。

下面的独立示例会为 HTML 文本节点编码姓名:

本节中的代码以及显示的标签或输出仍为英文。

<?php
$name = 'Ada & Sam';
echo htmlspecialchars($name, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'), PHP_EOL;
Ada &amp; Sam

提供的运行时证据确认了此输出。示例输出为 Ada &amp; Sam,这是供 HTML 文本上下文使用的字符串,不应拿来当作 SQL 或 JavaScript 使用。

使用 filter_var() 验证并规范化输入

filter_var() 会执行选定的验证或过滤操作;它不是 HTML 清理器。例如,filter_var($email, FILTER_VALIDATE_EMAIL) 会返回通过验证的电子邮件值;如果该值不符合过滤器的要求,则返回 false。filter_var($count, FILTER_VALIDATE_INT) 可以验证整数;如果应用要求特定范围,还可以使用相应选项。请明确检查返回值,因为 false 表示验证失败。

清理类过滤器可能会转换输入,但不能证明输入有效。例如,FILTER_SANITIZE_EMAIL 会移除该过滤器不允许的字符;它不能证明生成的地址属于某位用户,也不能证明该地址可以接收邮件。如果应用决定规范化输入,应先定义规则,再验证处理后的结果。不要把 FILTER_SANITIZE_STRING 当作通用解决方案;移除标记既不能提供适合具体上下文的输出编码,也不能让任意内容变得安全。

使用预处理语句执行 SQL

使用参数化查询,将 SQL 语法与用户提供的值分开。使用 PDO 时,先准备带占位符的查询,再在执行时单独传入值。例如,其模式是 $statement = $pdo->prepare('SELECT id, email FROM Users WHERE lastname = :lastname');,然后执行 $statement->execute(['lastname' => $lastname]);。这只是示意片段:$pdo 必须已经连接到目标数据库;数据库服务和架构不属于此示例的范围。

PDO 不会清理任意输入。对于查询中的值,参数绑定是防止 SQL 注入的保护措施;但它不会验证某个值是否符合应用的业务要求。应根据字段规则验证值,并为表名、列名等标识符采用单独的查询构造策略,因为这类标识符通常不能作为值参数传入。MySQLi 也支持预处理语句;使用该接口时,应采用它的参数绑定 API。

根据预期类型处理个人资料字段

为每个个人资料字段采用不同的规则。如有需要,应验证电子邮件地址,并通过应用流程确认其归属;只有在符合预期策略时才修剪显示名称;验证邮政编码时,应依据相应地区的规则,而不是假设所有地区都使用同一种格式。对于数字字段,应先解析并检查允许的范围,再使用该值。在字段规则允许的情况下,保留合法的标点和 Unicode 字符。

在页面中显示已存储的个人资料值时,即使提交时已经验证过,也应在输出时针对具体的 HTML 上下文进行编码。查询个人资料时,应通过预处理语句传入值。将验证、规范化、数据库参数化和输出编码作为彼此独立的决策,有助于理清处理过程,避免误以为某种转换能提供通用保护。