SQL注入之所以危险,根本原因在于程序把用户输入的数据当成了SQL代码的一部分来执行。当开发者直接把用户提交的参数拼接进SQL语句时,攻击者就可以通过精心构造的输入改变语句原有的逻辑,比如绕过登录校验、导出全库数据,甚至通过数据库函数执行系统命令。要根治这个问题,核心思路只有两条:一是让数据与代码彻底分离,也就是使用参数化查询;二是即便第一道防线被突破,数据库账号的权限也不足以造成更大的破坏。

一、SQL注入的成因:拼接式SQL到底错在哪里
先看一段典型的漏洞代码。这段PHP代码把用户输入的用户名直接拼进了SQL语句:
// 危险写法:直接拼接用户输入 $username = $_POST['username']; $password = $_POST['password']; $sql = "SELECT * FROM users WHERE username = '$username' AND password = '$password'"; $result = $mysqli->query($sql);
如果攻击者在用户名输入框中填入admin' --,最终执行的SQL就会变成SELECT * FROM users WHERE username = 'admin' --' AND password = '...'。--后面的密码判断被注释掉了,直接以admin身份登录成功。更严重的情况下,攻击者可以借助UNION SELECT读取其他表的数据,或者利用数据库特性逐步猜解表结构。
这个问题的本质是代码与数据混在了一起。数据库收到的只是一个完整字符串,它无法分辨哪部分是开发者写的逻辑,哪部分是用户填的数据。只要这种混淆存在,各种过滤、转义都只是补丁,无法从根本上消除风险。此外还要注意,拼接不仅出现在WHERE条件里,ORDER BY字段名、表名、LIMIT参数这些无法参数化的位置同样是注入高发区,需要用白名单校验来处理。
二、参数化查询:让数据归数据,代码归代码
参数化查询的原理是先发送带占位符的SQL语句骨架给数据库做预编译,数据库此时已经确定了语句的语法结构,之后再单独发送参数值。参数无论填什么内容,都只会被当作纯数据处理,永远不会改变语句的执行逻辑。这是目前公认最可靠的SQL注入防御手段。
在PHP中使用PDO的写法如下:
// 安全写法:PDO预处理语句
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :username AND password = :password");
$stmt->execute([
':username' => $username,
':password' => $password
]);
$user = $stmt->fetch();
Java中对应的是PreparedStatement,用法类似:
PreparedStatement ps = conn.prepareStatement(
"SELECT * FROM users WHERE username = ? AND password = ?");
ps.setString(1, username);
ps.setString(2, password);
ResultSet rs = ps.executeQuery();
Python的写法同样简洁,以pymysql为例:
cursor.execute(
"SELECT * FROM users WHERE username = %s AND password = %s",
(username, password)
)
使用参数化查询时要注意几个细节。第一,占位符只能代表值,不能代替表名、列名或ORDER BY字段,这些动态部分必须用白名单映射,例如预先定义允许的字段数组,根据用户传入的索引取值而不是直接使用原始输入。第二,PDO在默认模式下模拟预编译,建议设置PDO::ATTR_EMULATE_PREPARES为false,让真正的数据库驱动执行预编译。第三,使用MyBatis、Hibernate等框架时,务必使用#{}占位符而不是${},后者本质仍是字符串拼接,是大量漏洞的来源。
三、权限控制:纵深防御的第二道关卡
即使代码层面做得再好,也难免有遗漏的角落,比如遗留的老模块、第三方插件。这时数据库账号的权限就成了最后的保险丝。最小权限原则要求:应用连接数据库的账号只拥有完成任务所必需的权限,不多给一点。
具体落地时可以这样做。首先为应用创建专用账号,禁止使用root或sa等超级用户连接:
-- 创建只具备基本读写权限的应用账号 CREATE USER 'webapp'@'127.0.0.1' IDENTIFIED BY '强随机密码'; GRANT SELECT, INSERT, UPDATE ON shop.* TO 'webapp'@'127.0.0.1'; -- 不授予 DROP、ALTER、FILE、GRANT 等高危权限 FLUSH PRIVILEGES;
其次,可以利用视图隔离敏感数据。比如用户表中存有密码哈希和手机号,可以创建一个不含这些字段的视图,应用账号只授权访问视图而无权访问基表,这样即使注入成功,攻击者也拿不到最敏感的列。再次,删除、导出、批量更新等管理操作应使用独立的高权限账号,且只在运维流程中人工触发,Web应用永远碰不到这些权限。
另外几个配套措施同样重要:限制数据库账号的登录来源IP,只允许应用服务器内网连接;关闭远程root登录;生产环境屏蔽详细的SQL报错信息,避免攻击者通过报错内容探测表结构;对必须执行复杂业务逻辑的场景,可以封装为存储过程并只授予EXECUTE权限,应用无法直接操作底层表。
四、辅助手段与常见误区
除了上述两大核心手段,还有一些辅助措施值得配合使用。输入校验方面,对手机号、邮箱、数字ID等格式固定的字段做严格白名单校验,不符合格式的直接拒绝;对整数参数强制做intval或类型转换,从源头减少脏数据。部署层面,WAF可以拦截常见的注入特征,作为额外缓冲,但绝不能作为唯一防线。定期使用sqlmap等工具做自动化扫描,并对代码库中全局搜索拼接SQL的写法,能帮助发现存量漏洞。
有几个常见误区需要澄清。其一,认为做了转义就安全了,addslashes或手动替换引号在多字节字符集(如GBK)下存在绕过风险,而且不同数据库的转义规则不同,远不如预编译可靠。其二,认为NoSQL没有注入问题,MongoDB的$where、操作符注入同样是真实存在的威胁。其三,认为内部系统不用防,内网系统一旦被横向渗透,往往成为攻击链的跳板。
总结来说,防御SQL注入的正确姿势是分层布防:参数化查询解决代码与数据分离的根本问题,是第一道也是最关键的一道防线;最小化的数据库权限控制限制攻击成功的破坏范围;再加上输入校验、错误信息管控和定期审计,才能构建出真正经得起考验的安全体系。与其出事后紧急修补,不如在写第一条SQL语句时就养成正确的习惯。