动态标识符(表名、字段名等)必须用硬编码白名单校验,禁止拼接用户输入;PDO/mysqli预处理无法参数化此类结构,仅占位符可安全绑定数据。

动态表名/列名必须走白名单校验
PHP原生开发中,PDO::prepare() 和 mysqli::prepare() 无法参数化表名、字段名、排序方向(ORDER BY)、分组方式(GROUP BY)等SQL结构部分。一旦拼接进查询字符串,就等于把数据库结构控制权交到用户手里。
常见错误现象:PDOStatement::execute(): SQLSTATE[HY093]: Invalid parameter number(占位符被误用在表名位置),或更危险的直接执行 SELECT * FROM {$_GET['table']} 导致任意表读取。
- 只允许从预设数组中选取:
$allowed_tables = ['users', 'orders', 'products']; - 用
in_array()严格比对,不依赖类型宽松匹配:$table = in_array($_GET['table'], $allowed_tables, true) ? $_GET['table'] : 'users'; - 禁止任何转义、过滤、正则“清理”替代白名单——
addslashes()或preg_replace()对表名无效
排序字段和方向必须双白名单
用户通过 ?sort=created_at&order=DESC 控制 ORDER BY 时,created_at 是字段名,DESC 是关键字,二者都不可信。单独验证字段名但放行任意 order 值(如 ASC; DROP TABLE users--)仍可能触发注入(尤其在旧版 MySQL 宽字节或低版本驱动下)。
- 字段白名单:
$allowed_sort_fields = ['id', 'username', 'created_at', 'status']; - 方向白名单:
$allowed_sort_orders = ['ASC', 'DESC']; - 组合校验后拼接:
$sql .= " ORDER BY {$field} {$direction}";,其中$field和$direction都来自白名单判断结果
IN 子句参数数量不确定时怎么安全处理
比如搜索多个ID:WHERE id IN (1,2,3),用户传入的 $_GET['ids'] = ['1','2','3']。不能写成 "WHERE id IN (?)".str_repeat(",?", count($ids)-1) 然后 execute($ids) —— 这看似参数化,但占位符数量由用户输入决定,容易因长度超限或类型混杂出错。
立即学习“PHP免费学习笔记(深入)”;
- 先清洗数组:
$ids = array_map('intval', $_GET['ids']);,再过滤空值和零值 - 生成固定数量占位符:
$placeholders = str_repeat('?,', count($ids) - 1) . '?'; - 准备语句:
$stmt = $pdo->prepare("SELECT * FROM users WHERE id IN ({$placeholders})"); - 执行时绑定整组:
$stmt->execute($ids);
白名单必须硬编码或配置文件定义,不可从数据库读取
有人试图把白名单存在数据库里,方便后台管理,这反而引入新风险:如果攻击者已通过其他漏洞写入恶意表名到配置表,后续所有白名单校验就失效了。
真正安全的白名单是部署时确定、上线后不可变的。哪怕多维护几行代码,也比动态加载强。
最容易被忽略的一点:白名单校验必须在参数绑定之前完成,且不能被绕过——比如放在 if ($debug) 分支里,或藏在某个未调用的 helper 函数中。



















