作为一名后端开发,SQL 注入是必须跨过的一道坎。它连续多年位列 OWASP Top 10 榜首,是危害最大的 Web 漏洞之一。本文将带你彻底理解 SQL 注入的原理,并给出 7 条行之有效的防御措施。

一、什么是 SQL 注入?

SQL 注入是指攻击者通过在用户输入中插入恶意的 SQL 代码,欺骗后端服务器执行非预期的 SQL 命令,从而达到窃取数据、绕过认证、删库跑路等目的。

一个经典例子

假设登录接口的 SQL 查询为:

SELECT * FROM users WHERE username = '{$username}' AND password = '{$password}'

攻击者输入:

  • username: admin' --
  • password: 任意值

拼接后的 SQL 变成:

SELECT * FROM users WHERE username = 'admin' -- ' AND password = 'xxx'

-- 将后续的密码检查变成了注释,攻击者无需密码即可登录管理员账号。

二、SQL 注入的危害

  • 数据泄露:读取数据库中的敏感信息(用户资料、银行卡号)。
  • 身份伪造:绕过登录验证,获取管理员权限。
  • 数据篡改:修改、删除数据库记录。
  • 服务器沦陷:利用 INTO OUTFILE 写入 WebShell,进一步控制服务器。

三、如何有效防止 SQL 注入?

记住一个核心原则:永远不要相信用户输入。以下措施应当组合使用。

1. 使用预编译(PreparedStatement)—— 最有效的方法

预编译将 SQL 语句的结构与参数分离,数据库会先编译 SQL 模板,再将参数作为纯数据传递,因此参数中的 SQL 关键字不会被解析。

语言 正确示例
Java PreparedStatement stmt = conn.prepareStatement("SELECT * FROM user WHERE id=?");
stmt.setInt(1, userId);
PHP (PDO) $stmt = $pdo->prepare("SELECT * FROM user WHERE id = :id");
$stmt->execute(['id' => $id]);
Python cursor.execute("SELECT * FROM user WHERE id = %s", (user_id,))
Go db.QueryRow("SELECT * FROM user WHERE id = ?", userID)

注意:预编译只能保护 SQL 语句中的值,不能用于表名、列名或 ORDER BY 后的字段名。这些动态标识符需要额外白名单校验。

2. 对输入进行严格的类型校验与转义

当无法使用预编译时(例如需要动态拼接表名、列名),必须:

  • 类型校验:如使用 intval() 确保 ID 是整型。
  • 白名单过滤:例如 $allowedColumns = ['id', 'name']; if (!in_array($column, $allowedColumns)) { exit; }
  • 转义函数:如 MySQL 的 mysqli_real_escape_string(),但注意转义不能完全防御所有注入,不推荐依赖它。

3. 使用存储过程(谨慎使用)

存储过程同样需要传参,如果内部还是动态拼接 SQL,依然有注入风险。正确做法:使用存储过程配合参数化查询。

4. 最小权限原则

数据库账号应遵循最小必要权限:

  • 普通业务账号不应具有 DROPALTERFILE 等权限。
  • 只授予 SELECTINSERTUPDATEDELETE 必要权限。
  • 不同应用使用不同账号,隔离风险。

5. 对输出进行编码(纵深防御)

即使有 SQL 注入,如果能将返回的数据进行 HTML 编码,也可以缓解 XSS(跨站脚本)。但这对 SQL 注入本身无直接防御作用,只是减少连锁危害。

6. 使用 ORM 框架

ORM(如 Hibernate、MyBatis、Entity Framework、GORM 等)默认使用参数化查询,大大降低了手工拼接 SQL 的概率。但要注意:

  • 避免使用 ORM 提供的原生 SQL 拼接功能(如 MyBatis 的 ${})。
  • 始终使用参数绑定(如 MyBatis 的 #{})。

7. 部署 Web 应用防火墙(WAF)与定期扫描

  • WAF 可以拦截常见 SQL 注入攻击载荷。
  • 使用 sqlmap、AppScan 等工具定期扫描漏洞。
  • 开启数据库日志,监控异常查询。

四、常见误区

误区 正确观点
客户端 JS 校验就够了 攻击者可以绕过前端直接发请求,服务端必须校验。
使用正则过滤就能防注入 黑名单很难覆盖所有攻击变种(如编码绕过、内联注释)。预编译才是正解。
数字类型不需要处理 即使 id=1 可直接拼,也建议参数化,因为 1 AND 1=1 也可能是注入点。
存储过程是安全的 存储过程内部若使用动态 SQL 拼接,同样存在注入风险。

五、总结

防止 SQL 注入的最佳实践可以归纳为一条铁律:使用预编译 + 输入校验 + 最小权限 + ORM。其中,预编译(参数化查询)是目前公认最有效、最简单的方式。

在实际开发中,请严格遵守:

  • 任何外部数据(GET/POST/Cookie/Header)都不能直接拼接到 SQL 中。
  • 动态表名、列名必须通过白名单校验。
  • 定期进行安全审计和渗透测试。

只有把安全意识融入编码习惯,才能构建出更健壮的应用。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐