本文目录导读:

- 最核心、最有效的手段:使用参数化查询(PreparedStatement)
- 严格的输入验证与过滤(作为第二道防线)
- 最小权限原则(数据库层面)
- 使用ORM框架(对象关系映射)
- 对输出进行编码(防御反射型注入的次要关联)
- 技术层面的辅助手段
- 特殊场景的处理
- 最佳实践清单
SQL注入漏洞的防御需要从代码层面、数据库层面、架构层面以及运维层面进行多层防护,最核心的原则是:永远不要信任用户的输入。
以下是系统性的防御措施,按优先级排序:
最核心、最有效的手段:使用参数化查询(PreparedStatement)
这是防御SQL注入的黄金标准,它强制将SQL语句的结构(逻辑)与用户输入的数据(参数)分离,数据库会先编译SQL语句结构,再将参数作为纯数据绑定进去。
-
Java (JDBC):
// 错误示范(拼接字符串):极度危险 String sql = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'"; // 正确做法(参数化查询): String sql = "SELECT * FROM users WHERE username = ? AND password = ?"; PreparedStatement pstmt = connection.prepareStatement(sql); pstmt.setString(1, username); pstmt.setString(2, password); ResultSet rs = pstmt.executeQuery();
-
Python (PyMySQL / psycopg2):
# 错误示范:字符串格式化 sql = "SELECT * FROM users WHERE username = '%s'" % username # 正确做法:使用占位符 cursor.execute("SELECT * FROM users WHERE username = %s AND password = %s", (username, password)) -
.NET / C#: 使用
SqlCommand配合Parameters.AddWithValue。 -
PHP: 使用 PDO 或 MySQLi 的预处理语句。
-
Node.js (mysql2): 使用 占位符。
严格的输入验证与过滤(作为第二道防线)
参数化查询解决了大部分问题,但在以下场景仍需输入验证:
- 动态表名、列名或排序字段(参数化查询无法绑定表名/列名)。
- 将用户输入用于存储过程调用参数。
- 将用户输入用于
LIKE查询(仍需参数化,但需转义特殊通配符和)。
验证原则:
- 白名单验证(推荐):只允许特定的字符或格式,ID 必须是数字,用户名只能包含字母、数字和下划线。
- 黑名单过滤(不推荐):试图过滤掉 、、、 等关键字,很容易被绕过。
- 类型转换:如果期望是整数,强制转换为
int类型。
最小权限原则(数据库层面)
即使发生了注入,也要限制攻击者所能造成的损害。
- 连接数据库的账户权限要最小化:应用程序账户通常只需要
SELECT、INSERT、UPDATE、DELETE权限,永远不要使用root或sa等高权限账户。 - 禁止执行存储过程(除非必要):特别是
xp_cmdshell等危险存储过程。 - 使用存储过程:虽然不能完全防御注入(如果存储过程内拼接SQL同样危险),但良好的存储过程设计可以限制权限和操作粒度。
使用ORM框架(对象关系映射)
现代ORM框架(如 Hibernate、Entity Framework、SQLAlchemy、MyBatis-Plus 等)在内部强制使用参数化查询。
- 优点:开发者只要遵循框架规范(如使用
getById、QueryBuilder或原生查询的占位符),基本不会引入注入漏洞。 - 缺点:如果使用框架提供的“原生SQL查询”功能,并手动拼接字符串,同样危险。
对输出进行编码(防御反射型注入的次要关联)
虽然主要防御输入点,但在将数据(特别是从数据库查出的文本)回显到HTML页面时,进行HTML实体编码,可以防御存储型XSS,SQL注入和XSS经常相互配合。
技术层面的辅助手段
- Web应用防火墙 (WAF):可以检测并拦截常见的SQL注入攻击载荷(如
OR 1=1、UNION SELECT)。 - 数据库防火墙:监控并阻断异常的SQL语句(如大量查询、非授权的敏感表访问)。
- 启用参数化日志与错误处理:
- 禁止在正式环境中显示详细的数据库错误信息(如 HTTP 500 页面的堆栈信息),攻击者常利用错误信息构造注入。
- 记录所有SQL查询日志(注意不要记录明文密码),用于事后审计。
特殊场景的处理
- LIKE 查询:
SELECT * FROM articles WHERE title LIKE '%' + ? + '%'- 解法:参数化查询可以绑定
%keyword%这个整体字符串,但需要手动转义用户输入中的 和 (例如在Java中,用keyword.replace("%", "\\%").replace("_", "\\_"))。
- 解法:参数化查询可以绑定
- 动态排序:
ORDER BY ?无法参数化(因为列名不是参数)。- 解法:使用白名单验证,只允许
sort参数的值是id、name、create_time中的一个,否则拒绝。
- 解法:使用白名单验证,只允许
- IN 语句:
WHERE id IN (?)无法直接绑定数组。解法:根据列表长度动态生成多个占位符,或者使用ORM框架提供的集合绑定功能。
最佳实践清单
| 步骤 | 操作 | 优先级 |
|---|---|---|
| 1 | 所有与数据库交互的地方,强制使用参数化查询。 | 最高 |
| 2 | 使用ORM框架,并避免在其内部拼接原生SQL。 | 高 |
| 3 | 进行白名单验证(如检查数据类型、长度、正则)。 | 高 |
| 4 | 数据库账户遵循最小权限原则。 | 中 |
| 5 | 隐藏数据库错误详情,使用统一的错误提示。 | 中 |
| 6 | 在WAF或API网关层启用SQL注入防护规则。 | 中 |
| 7 | 定期代码审计和渗透测试。 | 持续 |
一句话总结: 不要用字符串拼接SQL,永远使用参数化查询(PreparedStatement),配合白名单验证和最小权限,SQL注入漏洞基本可以被杜绝。