一、引言
在当今数字化时代,数据库作为存储和管理数据的核心组件,其安全性至关重要。SQL 注入是一种常见且极具威胁性的数据库安全漏洞,它可能导致数据泄露、篡改甚至系统被完全控制。本文将深入探讨 SQL 注入漏洞的产生原因、表现形式以及如何有效地解决这一问题。
二、SQL 注入漏洞的产生效果
在已知用户名的情况下,攻击者可以通过输入特殊构造的密码,利用 SQL 注入漏洞绕过系统的身份验证机制,成功登录系统。例如,原本需要正确的用户名和密码才能访问的系统,攻击者只需输入特定的字符组合,如 "aaa'or'1=1" 作为密码(用户名已知),就可以在不知道正确密码的情况下登录进去,这显然严重破坏了系统的安全性和访问控制机制。
三、SQL 注入漏洞的产生原因
SQL 注入漏洞的产生主要源于应用程序在拼接 SQL 语句时没有对用户输入进行充分的验证和过滤。以常见的登录场景为例,后台程序拼接 SQL 语句的代码通常如下:
String sql = "select * from t_user where username = '"+username+"' and password = '"+password+"'";
在这种情况下,如果用户输入恶意的字符,就会改变 SQL 语句的逻辑。比如,当用户输入 "aaa'or'1=1" 作为密码时,拼接后的 SQL 语句变为:
String sql = "select * from t_user where username = 'aaa'or'1=1' and password = 'sfsdfsds";
由于 "or '1=1'" 恒为真,无论密码是否正确,该查询都能返回结果,从而导致攻击者绕过了密码验证。
按照 AND
优先级高于 OR
的规则,它会先计算 '1=1' and password = '123456'
这部分,然后再与前面的 username = 'aaa'
通过 OR
进行运算。
如果 '1=1'
恒为 true
,当 password = '123456'
结果为 false
时,'1=1' and password = '123456'
结果为 false
,但由于前面有 OR
连接 username = 'aaa'
,只要 username = 'aaa'
为 true
,整个表达式的结果就为 true
,这就可能导致不符合预期的查询结果,也是 SQL 注入利用的一个原理基础。
另一种常见的恶意输入是 "aaa'‐‐ '",拼接后的 SQL 语句变为:
String sql = "select * from t_user where username = 'aaa'‐‐ '' and password = 'sfsdfsdfs";
"‐‐" 在 SQL 中是注释符,这使得后面的密码验证部分被注释掉,查询只验证用户名,同样也实现了非法登录。
四、SQL 注入漏洞的解决方案
为了解决 SQL 注入漏洞,我们可以使用PreparedStatement
接口,它是Statement
的子接口,具有以下优势:
-
预编译功能 :
PreparedStatement
能够将 SQL 语句中的参数部分使用 "?"(占位符)来代替,并先将编写的 SQL 语句发送到 MySQL 服务器端进行编译。编译后的 SQL 语句格式固定,后续传入的任何值都会作为 "?" 的参数处理,从而避免了恶意输入改变 SQL 语句逻辑的情况。 -
具体使用方法 :
- 获取预编译对象 :通过
Connection
接口的prepareStatement(String sql)
方法来预编译 SQL 语句。例如:
Connection conn = JdbcUtils.getConnection();
String sql = "select * from y_user where username =? and password =?";
PreparedStatement stmt = conn.prepareStatement(sql); - 获取预编译对象 :通过
-
设置参数值 :使用
setXxxx()
系列方法(如setString()
、setInt()
等)向 "?" 传入值,参数的位置从 1 开始计数。例如:stmt.setString(1,username);
stmt.setString(2,password); -
执行 SQL 语句 :根据 SQL 语句的类型,使用
executeQuery()
方法执行查询操作(返回结果集),使用executeUpdate()
方法执行增删改操作。例如:ResultSet rs = stmt.executeQuery();
五、示例代码分析
以下是一个完整的 Java 代码示例,展示了如何模拟 SQL 注入漏洞以及如何使用PreparedStatement
解决该问题:
java
package cn.qcby.demo1;
import cn.qcby.utils.JdbcUtils;
import java.sql.*;
/**
* 演示SQL注入的问题,漏洞
* 在已知用户名的情况下,通过sql语言关键字,登录系统。密码随意输入的。
* SQL注入产生原因是SQL语句的拼接,利用SQL关键字产生效果。
* 需要解决SQL注入的问题
*
* 解决SQL注入问题,采用SQL语句预编译的方式,把SQL语句中的参数使用?占位符来表示,先把SQL语句编译,格式固定的。
* 再给?传入值,传入任何内容都表示值。数据库会判断SQL执行的结果。
*/
public class JdbcTest4 {
public static void main(String[] args) {
// 模拟登录的功能,有SQL注入的问题
//String result = new JdbcTest4().login("aaa'or'1=1", "1234sdf");
//System.out.println(result);
String result = new JdbcTest4().login2("aaa'or'1=1", "123456");
System.out.println(result);
}
/**
* 采用预编译的方式,解决SQL注入的问题
* @param username
* @param password
* @return
*/
public String login2(String username,String password){
Connection conn = null;
// 预编译执行SQL语句对象
PreparedStatement stmt = null;
ResultSet rs = null;
try {
// 获取到连接
conn = JdbcUtils.getConnection();
// 使用?占位符
String sql = "select * from y_user where username =? and password =?";
// 预编译SQL语句,把SQL语句固定
//Statement statement = conn.createStatement();
//statement不能防止sql注入问题 prepareStatement 能够防止sql注入问题
stmt = conn.prepareStatement(sql);
// 需要给?设置值
stmt.setString(1,username);
stmt.setString(2,password);
// 执行SQL语句
rs = stmt.executeQuery();
// 遍历数据
if(rs.next()){
// 表示存在数据,如果存在,说明用户名和密码编写正确
return "登录成功...";
}else{
return "登录失败了...";
}
} catch (SQLException e) {
e.printStackTrace();
}finally {
JdbcUtils.close(conn,stmt,rs);
}
return null;
}
/**
* 模拟登录的功能,通过用户名和密码从数据库中查询
* @param username
* @param password
* @return
*/
public String login(String username,String password){
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
try {
// 获取到连接
conn = JdbcUtils.getConnection();
// 编写SQL语句的拼接 '1=1' true password = '1234sdfsce' false true and false 整体上false
// String sql = "select * from t_user where username = 'aaa' or '1=1' and password = '1234sdfsce'";
// String sql = "select * from t_user where username = 'aaa' or false";
String sql = "select * from y_user where username = '"+username+"' and password = '"+password+"'";
// 执行sql
stmt = conn.createStatement();
// 执行
rs = stmt.executeQuery(sql);
// 遍历数据
if(rs.next()){
// 表示存在数据,如果存在,说明用户名和密码编写正确
return "登录成功...";
}else{
return "登录失败了...";
}
} catch (SQLException e) {
e.printStackTrace();
}finally {
JdbcUtils.close(conn,stmt,rs);
}
return null;
}
}