唯一可靠防sql注入的方法是参数化查询,必须禁用pdo模拟预处理(pdo::attr_emulate_prepares => false)、统一使用占位符绑定用户输入,表名/字段名等标识符须严格白名单校验,彻底杜绝字符串拼接。

光靠加 mysqli_real_escape_string 或正则过滤,根本没法根除 SQL 注入——它只是给漏洞打补丁,不是拆除引信。真正移除风险,必须把“用户输入参与 SQL 结构生成”这个行为从代码里彻底砍掉。
用 PDO 替换所有 mysql_* 函数并禁用模拟预处理
PHP 7.0+ 已彻底移除 mysql_connect、mysql_query 等函数,继续留着它们的调用,项目连启动都做不到。替换不是简单改名,关键在配置:
-
PDO::ATTR_EMULATE_PREPARES => false必须显式设为 false,否则prepare会退化成字符串拼接,防注入失效 - 连接字符串里别漏掉
;charset=utf8mb4,避免宽字符绕过(如 %df%27 这类编码) - 过程式写法(如
mysql_fetch_array)必须全部转成面向对象风格,否则绑定逻辑难统一管控
所有动态字段名/表名必须走白名单校验
参数化查询只管 WHERE 条件里的值,不管 ORDER BY 字段、IN 列表长度、表名前缀这些结构部分。这些地方拼接 = 直接开后门:
-
ORDER BY字段不能靠in_array($sort, ['name', 'created_at'], true)就完事——必须在 SQL 拼接前校验,且校验失败直接拒掉请求,不 fallback -
IN (?, ?, ?)的问号数量必须由代码计算,不能用implode(',', array_fill(0, count($ids), '?'))后再拼进 SQL 字符串 - 分表场景下,表名中的年份或哈希后缀(如
users_2026)必须来自配置或服务端计算,严禁从 URL 或 POST body 直接取
业务逻辑从 SQL 层上移到 PHP 层
很多注入藏在“看起来安全”的条件分支里,比如 WHERE status IN ('".implode("','", $statuses)."')。重构核心是:数据库只负责执行,不决定执行什么。
- 把
IN条件拆成多个OR查询 + PHP 合并结果,或者用临时表批量写入再 JOIN——只要不拼字符串 - 模糊搜索的
LIKE通配符(%)必须由 PHP 加,不能让用户传%admin%再原样塞进execute() - MyBatis 的
${}标签是硬伤,对应 XML 中所有<if test="xxx">ORDER BY ${field}</if>都得重写成枚举 + 显式 SQL 分支
收紧数据库账号权限并关闭错误回显
代码层修得再严,数据库账号权限过大,一次盲注或二次注入就能提权。这不是可选项,是上线前必须卡死的底线:
- 生产环境账号禁止
FILE、PROCESS、SHOW DATABASES权限——SELECT、INSERT、UPDATE、DELETE按业务模块拆分授予 - PHP 配置中
display_errors = Off、log_errors = On,防止报错泄露表名、字段名、MySQL 版本 - 连接池或持久连接场景下,确保每次
new PDO都带完整 DSN 和选项,避免复用旧连接导致PDO::ATTR_EMULATE_PREPARES配置被忽略
最难的不是写对那几行 prepare 和 execute,而是把所有“动态拼 SQL”的惯性思维清零——只要还存在一处 "SELECT * FROM ".$table." WHERE ...",整套防护就等于没做。











