本文目录导读:
- 使用参数化查询(Prepared Statements)—— 最核心的防御
- 使用ORM(对象关系映射)框架
- 严格输入验证与白名单(作为补充)
- 数据库最小权限原则
- 数据库语句与存储过程
- 转义用户输入(仅作为最后防线且不推荐)
- 需要特别注意的“陷阱”场景
- 安全优先级排序
防止SQL注入是Web开发中最基本的安全要求之一,核心原则是:永远不要信任用户的任何输入,永远不要将用户输入直接拼接到SQL语句中。
以下是经过验证的、最有效的防御措施,按推荐程度排序:
使用参数化查询(Prepared Statements)—— 最核心的防御
这是唯一能够彻底杜绝SQL注入的根本方法,它将SQL语句的“结构”与“数据”分离开来。
-
原理:数据库驱动程序会预先编译SQL语句的结构(如
SELECT * FROM users WHERE id = ?),然后将用户输入作为纯数据(而非可执行的代码)传递给占位符(),无论用户输入什么奇葩字符(如' OR '1'='1),数据库都只会把它当作字符串字面量处理。 -
示例:
- PHP (PDO):
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?"); $stmt->execute([$userInput]); - Python (psycopg2):
cursor.execute("SELECT * FROM users WHERE id = %s", (user_input,)) - Java (JDBC):
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE id = ?"); stmt.setInt(1, userInput); - Node.js (mysql2):
connection.execute('SELECT * FROM users WHERE id = ?', [userInput], (err, results) => { ... })
- PHP (PDO):
使用ORM(对象关系映射)框架
ORM 框架(如 Java 的 Hibernate、Python 的 SQLAlchemy、PHP 的 Doctrine、Ruby on Rails 的 ActiveRecord 等)在底层已经封装好了参数化查询。
- 优点:你通常不需要再手动写原生 SQL,框架会帮你处理转义和参数绑定。
- 注意:不要在 ORM 中使用“原生查询”功能(
createNativeQuery)去拼接字符串,如果必须使用原生查询,请务必使用框架提供的参数绑定机制。
严格输入验证与白名单(作为补充)
即使使用了参数化查询,对输入进行验证也是一个良好的安全习惯,它可以防范业务逻辑漏洞和存储型XSS等其他攻击。
- 原则:白名单优于黑名单,明确允许哪些字符或格式,而不是禁止哪些字符。
- 数字:
is_numeric(),ctype_digit(),或intval()转换后判断范围。 - 邮箱:使用正则表达式验证格式(如 RFC 5322 兼容的简单版本)。
- 用户名:限制只允许字母、数字、下划线、连字符(如
^[a-zA-Z0-9_\-]{3,20}$)。
- 数字:
- 目标:在数据进入数据库之前,确保它符合预期,而不是用来阻止注入,参数化查询才是阻止注入的主角。
数据库最小权限原则
即使你的应用被注入了,也可以通过限制数据库账号的权限来降低损失。
- 规则:
- 为应用程序使用的数据库账号,只授予它完成业务所必需的最小权限。
- 一个只读的查询接口,只能用
SELECT权限;一个用户注册接口,只能用INSERT权限。 - 绝对不要让应用使用
root或DBA等高权限账号连接数据库。 - 严格限制
DROP、ALTER、CREATE、FILE(读文件)、PROCESS(看进程)等危险权限。
数据库语句与存储过程
- 存储过程:可以封装复杂的 SQL 逻辑,并使用参数化调用的方式(调用时用
CALL proc_name(?)),同样能防止注入。 - 视图:可以为特定的查询创建视图,应用只能访问视图,无法直接修改底层表。
转义用户输入(仅作为最后防线且不推荐)
如果你因为某些极端原因(如某个古老的数据库驱动不支持参数化)不得不拼接 SQL 字符串,那么必须使用数据库驱动提供的专用转义函数。
- 示例:PHP 的
mysqli_real_escape_string()。 - 警告:
- 开发者很容易忘记在某些地方转义(
ORDER BY后的字段名、表名、LIKE子句等)。 - 不适用于所有字符集(如使用 GBK 时可能发生宽字节注入)。
- 能不用就不用,能用参数化查询就绝对不要用转义。
- 开发者很容易忘记在某些地方转义(
需要特别注意的“陷阱”场景
ORDER BY和LIMIT后的参数:这些位置不能使用简单的参数占位符(),因为它们是 SQL 指令的组成部分,不是数据。- 防御方法:使用白名单,
allowed_order = ['name', 'date', 'id'] if user_input in allowed_order: sql = f"SELECT * FROM products ORDER BY {user_input}" else: # 禁止非法输入,返回错误 raise ValueError("Invalid sort field")
- 防御方法:使用白名单,
LIKE子句中的特殊字符:即使用户输入通过占位符传给了LIKE,但 和 字符在 LIKE 中有特殊含义,需要在应用层对用户输入中的 和 进行转义(str_replace(['%', '_'], ['\\%', '\\_'], $input))。
安全优先级排序
- 最推荐:参数化查询(Prepared Statements)(处理数据和
WHERE子句) - 次推荐:ORM 框架 + 输入验证白名单
- 必须做:最小权限数据库账号
- 避免做:
mysqli_real_escape_string等转义函数(只在不得已时用) - 绝对禁止:直接拼接字符串
SELECT * FROM users WHERE username = '{$_POST['user']}'
一句话口诀:能不用SQL拼接就不用SQL拼接,能参数化就绝不拼字符串。
标签: 输入验证