必须用mysqli::prepare()配合bind_param()或pdo关闭模拟预处理并设attr_emulate_prepares=false,且绑定前设utf8mb4字符集、正确指定类型、白名单校验非数据部分,否则仍存在sql注入风险。

mysqli::prepare() + bind_param() 必须用对,否则等于没防
直接拼接字符串的写法(比如 "SELECT * FROM users WHERE id = " . $_GET['id'])是注入温床,但光用 mysqli::prepare() 不代表安全——必须配合 bind_param() 才真正生效。常见错误是只调用了 prepare(),却把变量直接塞进 SQL 字符串里,结果还是拼接。
- 绑定前必须确保连接字符集已设为
utf8mb4:mysqli_set_charset($conn, 'utf8mb4'),否则宽字节注入可能绕过 -
bind_param()的类型标识不能乱填:"s"(字符串)、"i"(整数)、"d"(浮点)、"b"(BLOB),类型错会导致隐式转换或绑定失败 - 不要在占位符位置拼接表名、字段名或排序方向——这些不属于“数据”,无法参数化,需白名单校验(如
$order = in_array($_GET['sort'], ['name', 'created_at']) ? $_GET['sort'] : 'id')
PDO 默认开启模拟预处理,不关等于裸奔
PDO::ATTR_EMULATE_PREPARES 默认是 true,此时 prepare() 只是 PHP 在内存里做字符串替换,根本没发给 MySQL,和手动拼接无异。攻击者仍可构造 ' OR 1=1 -- 等 payload 成功注入。
- 初始化 PDO 时必须显式关闭:
PDO::ATTR_EMULATE_PREPARES => false - 确认 MySQL 版本 ≥ 5.1.17(旧版本不支持原生预处理),可用
$pdo->getAttribute(PDO::ATTR_CLIENT_VERSION)查看驱动版本 - 命名参数(
:name)和问号(?)安全性一致,选哪个只影响可读性,不建议混用
别信 mysql_real_escape_string() 或 str_replace() 这类“过滤”
这类函数只是简单转义或删除字符,在现代绕过手法面前形同虚设。比如攻击者用十六进制编码 0x27 表示单引号,或利用 MySQL 的宽字节特性(如 %A1%AA)触发截断,都能让 mysql_real_escape_string() 失效。
-
mysql_*函数在 PHP 7.0+ 已彻底移除,还在用就直接报Fatal error -
filter_var()、intval()、正则白名单等适合做输入验证,但不能替代预处理——它们是辅助层,不是主防线 - 密码字段绝不能用
md5()或sha1()存储,要用password_hash();即使被注入,也无法反推明文
生产环境数据库账号权限必须最小化
很多 PHP 一键环境(如 phpStudy、XAMPP)默认用 root 连接数据库,一旦 SQL 注入得手,攻击者能执行 DROP TABLE、LOAD_FILE 甚至写 shell 到磁盘。
- 为每个应用单独建用户,只授予
SELECT、INSERT、UPDATE、DELETE(按需),禁用GRANT、FILE、EXECUTE权限 - 生产环境务必关闭
display_errors,避免泄露表结构、字段名等敏感信息 - 错误日志要写到非 Web 可访问路径,且记录内容不包含原始 SQL 或用户输入
预处理语句本身不难写,难的是每一条查询都坚持用、用对、用全——漏掉一次,整个防护体系就塌一角。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











