多表联查时表名、字段名、JOIN条件等SQL结构部分必须用白名单校验,不可参数化;WHERE等数据位置可用PDO预处理;ORDER BY、LIMIT等动态部分须单独白名单+类型强转。

多表联查时不能把用户输入拼进 JOIN 或 WHERE 表名/字段名
参数化查询只对数据值有效,JOIN users ON orders.user_id = users.id 这类结构里的 users、orders、user_id 都是 SQL 结构的一部分,数据库不接受占位符。一旦你写成 "JOIN " . $_GET['table'] . " ON ...",就等于直接开门放攻击者进来。
常见错误现象:用 ORDER BY ? 试图参数化排序字段,结果 PDO/MySQLi 报错或静默忽略,最终 fallback 成字符串拼接;或者用 filter_var($_GET['sort'], FILTER_SANITIZE_STRING) 这种“清洗”方式,根本拦不住 Unicode 变体或宽字节绕过。
- 动态表名、列名、JOIN 条件字段必须走白名单校验,不是“过滤”,是“非此即彼”
- 例如只允许
['users', 'orders', 'products']和['id', 'created_at', 'status'],其他一概拒绝 - 白名单要硬编码或从配置数组读取,别存在数据库里——否则白名单本身可能被注入篡改
PDO prepare() 对多表查询的 WHERE 值依然有效,但要注意绑定顺序和类型
只要用户输入只出现在 WHERE、HAVING、VALUES 等纯数据位置,PDO 的 prepare() + execute() 就完全适用,和单表没区别。但多表场景下容易因字段歧义或类型混用出问题。
比如 SELECT u.name, o.amount FROM users u JOIN orders o ON u.id = o.user_id WHERE u.status = ? AND o.created_at > ?,两个 ? 必须按顺序传入对应值,且注意时间字段最好用 DateTime 对象或标准格式字符串,避免 MySQL 自动类型转换引发意外行为。
- 命名占位符(如
:status,:since)比问号更安全,尤其在字段多、顺序易错的多表查询中 - 数值型参数建议显式强转,如
(int)$_GET['limit'],再传给execute(),防止字符串被当作文本参与比较 -
LIKE子句中通配符必须在 PHP 层加,不能写成WHERE name LIKE ?然后传"%{$input}%"—— 这会导致$input里自带%或_被误解释
复杂动态条件(如多选筛选)别手写 SQL 拼接,用构建器或分层校验
用户勾选“状态=已支付、时间范围=近7天、分类=电子、关键词=手机”,这种组合查询最容易诱使开发者写 $sql .= " AND status = '" . $_GET['status'] . "'"。哪怕只对其中一个字段漏校验,整条链就崩了。
可行做法是:先定义合法字段映射表,再逐个校验并收集键值对,最后统一生成参数化语句。例如:
$allowed_fields = [
'status' => ['paid', 'pending', 'cancelled'],
'category' => ['electronics', 'books', 'clothing'],
'date_range' => 'date_interval'
];
$params = [];
$wheres = [];
if (isset($_GET['status']) && in_array($_GET['status'], $allowed_fields['status'])) {
$wheres[] = "o.status = ?";
$params[] = $_GET['status'];
}
if (isset($_GET['category']) && in_array($_GET['category'], $allowed_fields['category'])) {
$wheres[] = "p.category = ?";
$params[] = $_GET['category'];
}
// 最后组装:WHERE " . implode(' AND ', $wheres)
- 每个动态条件分支都独立校验,不依赖“前面已经安全了”的假设
- 字段名(如
o.status)必须硬编码,不可来自用户输入 - 枚举类参数(如状态、分类)必须用
in_array()白名单,不用正则或filter_var—— 后者无法保证值在业务语义上合法
ORDER BY 和 LIMIT 的用户输入最容易被忽略,但风险极高
很多人以为 ORDER BY 只控制显示顺序,无害。实际上,ORDER BY (SELECT password FROM users LIMIT 1) 是合法语法,能触发子查询泄露;LIMIT 1, 999999999 可导致全表扫描拖垮数据库。
所以 $_GET['sort'] 和 $_GET['limit'] 必须单独处理,不能混进主查询绑定流程。
-
ORDER BY字段只允许白名单中的列名,且必须限定所属表别名,如['u.name', 'o.created_at', 'p.price'],禁止裸字段 -
LIMIT参数必须是整数,且设合理上限,如min((int)$_GET['limit'], 100) - 若需支持升序/降序,
$_GET['order']只允许ASC或DESC,拼接前做in_array($dir, ['ASC', 'DESC'])判断
真正难的不是写对一条 SELECT,而是每次新增一个筛选项、换一种 JOIN 方式、加一个导出功能时,都重新过一遍白名单和绑定逻辑。漏掉一次,防护就形同虚设。











