MySQL 防止 SQL 注入的几种写法

=========================

SQL 注入长期位居 OWASP Top 10 前列,而 MySQL 作为全球使用最广泛的开源数据库,自然是攻击者的重点目标。很多开发者对 SQL 注入的理解仍停留在“过滤单引号”的层面,实际上现代注入手法早已覆盖报错注入、盲注、堆叠注入、二次注入等多种路径。本文从代码层面出发,梳理 MySQL 场景下防止 SQL 注入的几种主流写法,并对比其适用边界。

一、理解 SQL 注入的成因

SQL 注入的本质是用户输入被当作 SQL 代码执行。例如:

$sql = "SELECT * FROM users WHERE name = '$_GET[name]'";

name 传入 ' OR '1'='1 时,语句逻辑被篡改,攻击者可绕过认证甚至拖库。要根治这个问题,核心原则只有一条:让数据永远保持为数据,不能进入代码层

二、预处理语句(Prepared Statement)

这是目前最推荐的方案。预处理语句将 SQL 模板与参数分离,数据库先编译模板,再绑定数据,参数不会被解析为 SQL 语法。

1. PDO 写法(PHP)

$stmt = $pdo->prepare('SELECT * FROM users WHERE name = :name AND age > :age');
$stmt->execute([':name' => $name, ':age' => $age]);
$rows = $stmt->fetchAll();

注意:PDO::ATTR_EMULATE_PREPARES 建议设为 false,使用 MySQL 原生预处理,避免模拟预处理在特殊字符集下被绕过。

2. mysqli 写法

$stmt = $mysqli->prepare('SELECT * FROM users WHERE name = ?');
$stmt->bind_param('s', $name);
$stmt->execute();

3. Java / JDBC

PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE name = ?");
ps.setString(1, name);
ResultSet rs = ps.executeQuery();

4. Python / PyMySQL

cursor.execute("SELECT * FROM users WHERE name = %s", (name,))

需要特别提醒:表名、列名、ORDER BY 字段不能作为绑定参数。如果业务需要动态排序,必须使用白名单校验:

$allowed = ['id', 'name', 'created_at'];
$order = in_array($_GET['order'], $allowed) ? $_GET['order'] : 'id';

三、参数化查询的常见误区

不少团队以为用了 ORM 就安全,其实不然:

  • 字符串拼接 ORM 原生 SQLModel::whereRaw("name = '$name'") 依然存在注入。
  • LIKE 查询WHERE name LIKE '%?%' 是无效写法,应写成 WHERE name LIKE CONCAT('%', ?, '%')
  • IN 子句:不能直接绑定数组,需要根据元素个数动态生成占位符。
  • 二次注入:数据先存入数据库,后续取出再拼接进 SQL,预处理也救不了,必须在每次拼接处都遵循参数化原则。

四、输入验证与白名单

参数化并非万能,遇到无法绑定的场景(如动态表名、LIMIT 数值),白名单是最后一道防线:

$table = in_array($t, ['users', 'orders']) ? $t : 'users';
$limit = max(1, min((int)$_GET['limit'], 100));

对于数值型参数,强制类型转换 (int)(float) 通常足够;对于字符串,使用 preg_match 限定字符集:

if (!preg_match('/^[a-zA-Z0-9_]{1,20}$/', $username)) {
    throw new InvalidArgumentException('非法用户名');
}

五、转义函数:兜底而非首选

mysqli_real_escape_string()PDO::quote() 可以对特殊字符转义,但它们依赖字符集设置,若连接字符集与数据库不一致(如 GBK 宽字节注入),仍可能被绕过。因此转义只应作为历史代码的过渡手段,新项目一律使用预处理。

六、最小权限原则

即使代码存在漏洞,合理的数据库权限也能限制损失:

  • 应用账号只授予 SELECT / INSERT / UPDATE / DELETE,禁止 DROPFILEGRANT
  • 禁止应用账号访问 mysqlinformation_schema 等系统库(除非确有需要)。
  • 不同业务使用不同数据库账号,避免一处失守全库沦陷。
CREATE USER 'app'@'10.0.0.%' IDENTIFIED BY 'strong_password';
GRANT SELECT, INSERT, UPDATE, DELETE ON app_db.* TO 'app'@'10.0.0.%';

七、WAF 与日志监控

WAF 可以拦截常见注入特征(如 UNION SELECTSLEEP(INFORMATION_SCHEMA),但攻击者可通过编码、注释、大小写混写绕过,因此 WAF 只能作为纵深防御的一层,不能替代代码修复。同时建议开启 MySQL 慢查询日志和通用日志,对异常 SQL 做告警:

[mysqld]
general_log = ON
general_log_file = /var/log/mysql/general.log

八、总结

防止 SQL 注入的核心可以浓缩为一句话:永远不要拼接用户输入到 SQL 中。优先级排序如下:

  1. 预处理语句 + 参数绑定(首选)
  2. 白名单校验动态标识符
  3. 强制类型转换数值参数
  4. 最小权限数据库账号
  5. WAF 与日志监控作为补充

安全不是某个函数能解决的问题,而是编码习惯、架构设计与运维策略共同作用的结果。把预处理写进团队的代码规范,配合代码审计与自动化扫描,才能真正把 SQL 注入挡在门外。

未经允许不得转载:任鹏个人博客 » MySQL 防止 SQL 注入的几种写法

赞 (0) 打赏

评论 0

取消
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏