唯一可靠方式是用PDO::prepare()配合参数绑定,禁用任何字符串拼接;因拼接使用户输入直接参与SQL解析,易遭注入,且addslashes()等无法防御宽字节绕过或数字型隐式转换漏洞;表名、字段名等标识符必须白名单校验,不可绑定。

唯一可靠方式是用 PDO::prepare() 配合参数绑定,任何字符串拼接都必须被禁止——哪怕只是拼表名或字段名。
为什么不能用字符串拼接构造SQL
拼接语句(如 "SELECT * FROM " . $_GET['table'] . " WHERE id = " . $_GET['id'])会让用户输入直接参与 SQL 解析。攻击者传入 users WHERE 1=1 -- 或 users; DROP TABLE users; 就可能触发注入。即使加了 addslashes() 或 mysqli_real_escape_string(),也挡不住宽字节、多字节编码绕过,更无法防护数字型参数未加引号导致的类型隐式转换漏洞。
PDO::prepare() 的正确绑定姿势
占位符只接受值(value),不接受标识符(identifier)。以下写法全部错误:
$stmt = $pdo->prepare("SELECT * FROM $_GET[table]");$stmt = $pdo->prepare("ORDER BY $_GET[sort] ASC");$stmt = $pdo->prepare("WHERE status = '" . $_GET['status'] . "'");
正确做法是:
立即学习“PHP免费学习笔记(深入)”;
- 结构固定部分写死,动态值全用
?或:name占位 - 绑定时显式指定类型:
$stmt->bindValue(':id', $_GET['id'], PDO::PARAM_INT) - 避免省略第三个参数,否则数字可能被当字符串传,联合索引失效、JSON 比较失败等边界问题就来了
- 设
PDO::ATTR_EMULATE_PREPARES => false,强制走 MySQL 服务端真实预编译
表名、字段名、排序方向这些怎么处理
它们不能进绑定,只能靠白名单硬控。没有例外,没有“差不多安全”的正则替代方案。
- 定义允许列表:
$allowed_tables = ['users', 'orders', 'products']; - 校验必须严格:
if (!in_array($_GET['table'], $allowed_tables, true)) { die('Invalid table'); } - 字段名同理,
ORDER BY后的列和方向(ASC/DESC)也要分别白名单校验 - 禁止用
ctype_alnum()或简单正则放行,比如/^[a-zA-Z_][a-zA-Z0-9_]*$/仍可能绕过(如含反斜杠、NUL 字符等)
容易被忽略的执行环节陷阱
写了 prepare() 不等于安全了。常见翻车点:
- 调用
exec()执行带占位符的语句——它根本不支持绑定,占位符会被原样当字符串处理 - 用
query()直接执行拼接后的 SQL,哪怕前面刚调过prepare() - 事务中混用预处理和普通
query(),权限控制失效后一条DROP TABLE就能清空库 - 数据库账号有
DROP、ALTER权限,白名单漏判一个表名,后果就是不可逆的数据损毁
真正关键的不是“有没有用预处理”,而是每一处用户输入进入 SQL 的路径,是否都被明确归类为「可绑定值」或「需白名单的结构片段」——少一次判断,就多一分风险。



















