sql注入漏洞
目录
一、SQL注入概述
例子背景
正常情况下的查询
SQL注入攻击
利用优先级进行攻击
二、解决SQL注入
使用PreparedStatement接口
步骤和方法
1. 创建PreparedStatement对象
2. 向占位符传入值
3. 执行SQL语句
示例
总结
SQL 注入是一种常见的网络攻击手段。通俗来讲,很多网站、应用程序等都需要和数据库交互,比如登录时要查询数据库验证用户名和密码是否正确,接下来就以最常见的登录问题入手。
一、SQL注入概述
SQL注入问题本质上是由字符串拼接和And与or的优先级造成的(and优先级高于or)
让我们通过一个具体的例子来理解这句话:“SQL注入问题的本质是因为字符串拼接和AND
的优先级高于OR
”。
例子背景
假设我们有一个简单的登录表单,后端使用如下的SQL查询来验证用户名和密码:
SELECT * FROM users WHERE username = '输入的用户名' AND password = '输入的密码';
如果开发者直接将用户输入拼接到SQL查询中,而不进行任何处理,就可能导致SQL注入问题。
正常情况下的查询
正常情况下,用户输入的用户名和密码如下:
-
用户名:
admin
-
密码:
password123
拼接后的SQL查询如下:
SELECT * FROM users WHERE username = 'admin' AND password = 'password123';
这个查询会检查数据库中是否存在用户名为admin
且密码为password123
的用户。
SQL注入攻击
攻击者发现可以输入特殊构造的输入来绕过验证。假设攻击者输入如下:
-
用户名:
admin' --
-
密码:任意值(例如:
123456
)
拼接后的SQL查询如下:
SELECT * FROM users WHERE username = 'admin' --' AND password = '123456';
由于SQL中--
是注释符号,--
后面的内容会被SQL解析器忽略,所以实际执行的查询变为:
SELECT * FROM users WHERE username = 'admin' AND password = '';
这个查询会检查数据库中是否存在用户名为admin
且密码为空的用户。
利用优先级进行攻击
攻击者还可以利用AND
的优先级高于OR
的特性进行攻击。假设攻击者输入如下:
-
用户名:
admin' OR '1'='1
-
密码:任意值(例如:
123456
)
拼接后的SQL查询如下:
SELECT * FROM users WHERE username = 'admin' OR '1'='1' AND password = '123456';
由于AND
的优先级高于OR
,所以实际执行的查询如下:
SELECT * FROM users WHERE (username = 'admin') OR (('1'='1') AND password = '123456');
这意味着:
-
首先计算
username = 'admin'
。 -
然后计算
('1'='1') AND password = '123456'
。由于'1'='1'
总是为真,所以这个表达式简化为true AND password = '123456'
,即password = '123456'
。 -
最后,整个表达式变为
username = 'admin' OR password = '123456'
。
那么这就意味着,在这种情况下,不管用户输什么进去,只要username能够输对,password不论输什么都能登陆成功。
二、解决SQL注入
使用PreparedStatement接口
PreparedStatement
是Statement
的子接口,它提供了预编译SQL语句的功能,从而可以有效防止SQL注入漏洞。预编译SQL语句的关键在于使用占位符(?
)来代替参数部分,这样可以先将SQL语句发送到数据库服务器进行编译,编译后的SQL语句格式是固定的,后续传入的任何值都会作为参数处理,而不会被解释为SQL代码。
步骤和方法
1. 创建PreparedStatement对象
通过Connection
接口提供的prepareStatement
方法来创建PreparedStatement
对象。这个方法需要传入一个SQL语句,其中参数部分用?
占位符表示。
Connection conn = // 获取数据库连接
String sql = "SELECT * FROM users WHERE username = ? AND password = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
2. 向占位符传入值
使用PreparedStatement
提供的方法向占位符传入值。这些方法包括setInt
、setString
、setXXX
等,具体取决于参数的类型。
String username = "admin";
String password = "password123";
pstmt.setString(1, username); // 将第一个占位符替换为username
pstmt.setString(2, password); // 将第二个占位符替换为password
3. 执行SQL语句
根据执行的SQL语句类型,使用executeQuery
或executeUpdate
方法来执行查询。
-
executeQuery
:用于执行查询语句,返回ResultSet
对象。 -
executeUpdate
:用于执行增删改语句,返回受影响的行数。
ResultSet rs = pstmt.executeQuery();
while (rs.next()) {
// 处理查询结果
}
// 或者执行增删改操作
int affectedRows = pstmt.executeUpdate();
示例
假设我们有一个用户登录功能,需要查询数据库中是否存在匹配的用户名和密码。使用PreparedStatement
可以安全地实现这一功能。
import java.sql.*;
public class UserLogin {
public static void main(String[] args) {
String jdbcURL = "jdbc:mysql://localhost:3306/yourdatabase";
String username = "root";
String password = "yourpassword";
String sql = "SELECT * FROM users WHERE username = ? AND password = ?";
try (Connection conn = DriverManager.getConnection(jdbcURL, username, password);
PreparedStatement pstmt = conn.prepareStatement(sql)) {
String inputUsername = "admin";
String inputPassword = "password123";
pstmt.setString(1, inputUsername);
pstmt.setString(2, inputPassword);
ResultSet rs = pstmt.executeQuery();
if (rs.next()) {
System.out.println("登录成功!");
} else {
System.out.println("用户名或密码错误!");
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
总结
通过使用PreparedStatement
,我们可以有效地防止SQL注入漏洞。预编译SQL语句的关键在于使用占位符(?
)来代替参数部分,这样可以先将SQL语句发送到数据库服务器进行编译,编译后的SQL语句格式是固定的,后续传入的任何值都会作为参数处理,而不会被解释为SQL代码。这样可以确保即使用户输入恶意代码,也不会影响数据库的安全性。