Java中的SQL注入及如何轻松预防
什么是SQL注入?
SQL注入是十大网络应用程序漏洞之一。简单来说,SQL注入指的是通过用户输入的数据在查询中注入/插入SQL代码。它可能出现在任何使用关系数据库的应用程序中,如Oracle、MySQL、PostgreSQL和SQL Server。
为了进行SQL注入,恶意用户首先尝试在应用程序中找到一个可以嵌入SQL代码和数据的地方。这可以是任何网络应用程序的登录页面或其他任何地方。因此,当应用程序接收到嵌入SQL代码的数据时,SQL代码将与应用程序查询一起被执行。
SQL注入的影响
- A malicious user can obtain unauthorized access to your application and steal data.
- They can alter, delete data in your database and take your application down.
- A hacker can also get control of the system on which database server is running by executing database specific system commands.
SQL注入是如何工作的?
假设我们有一个名为tbluser的数据库表,用于存储应用程序用户的数据。 userId 是该表的主列。我们在应用程序中有一个功能,通过userId可以获取信息。userId的值通过用户请求来接收。
我们来看一下下面的示例代码。
String userId = {get data from end user};
String sqlQuery = "select * from tbluser where userId = " + userId;
1. 有效的用户输入。
当使用有效的数据执行上述查询,即userId值为132时,执行结果将如下所示。
输入数据:132
执行查询:选择*从tbluser表中,其中userId = 132。
结果:查询将返回userId为132的用户的数据。在这种情况下没有进行SQL注入。
2. 黑客使用者输入
黑客可以使用像Postman、cURL等工具修改用户请求,将SQL代码作为数据发送,从而绕过任何UI端验证。
输入数据: 2或1=1
执行查询:选择*从tbluser中,其中userId=2或1=1。
结果:现在上述查询具有使用SQL OR表达式的两个条件。
- userId=2: This part will match table rows having userId value as ‘2’.
- 1=1: This part will be always evaluate as true. So Query will return all the rows of the table.
SQL注入的类型
让我们来看看四种类型的SQL注入攻击。
1. 基于布尔逻辑的SQL注入
以上示例是布尔类型的SQL注入案例。它使用可以评估为真或假的布尔表达式。它可用于从数据库中获取额外的信息。例如;
输入数据:2或1=1
SQL查询:从tbl_employee表中选择first_name和last_name字段,其中empId等于2或1等于1。
2. 基于联合的SQL注入
SQL的联合操作符将具有相同列数的两个不同查询的数据合并在一起。在这种情况下,联合操作符用于从其他表中获取数据。
输入数据:2联合选择tbluser表中的用户名和密码。
查询:从tbl_employee表中选择first_name、last_name,其中empId=2;并联合查询从tbluser表中选择username、password。
请注意,为了满足您的需求,我提供的翻译是基于语句的字面意思,并不能保证在实际应用中的准确性。
使用基于联合的SQL注入,攻击者可以获取用户凭证。
3. 基于时间的SQL注入攻击
在基于时间的SQL注入中,会将特殊函数注入到查询中,这些函数可以暂停执行一段指定的时间。这种攻击会使数据库服务器减速。通过影响数据库服务器的性能,它可能导致应用程序崩溃。例如,在MySQL中:
输入数据:2 + 睡眠(5)
查询:从tbl_employee表中选择emp_id,first_name,last_name,其中empId=2 + SLEEP(5)。
在上面的例子中,查询执行将会暂停5秒钟。
4. 基于错误的SQL注入
在这种变体中,攻击者试图从数据库中获取错误代码和信息。攻击者会注入语法上不正确的SQL,以便数据库服务器返回错误代码和信息,从而获得数据库和系统信息。
Java SQL注入示例
我们将使用一个简单的Java Web应用程序来演示SQL注入。我们有一个Login.html文件,这是一个基本的登录页面,用于接收用户的用户名和密码,并将其提交给LoginServlet。
LoginServlet从请求中获取用户名和密码,并将它们与数据库中的值进行验证。如果验证成功,则Servlet将用户重定向到主页;否则,它将返回一个错误消息。
登录页面.html代码:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Sql Injection Demo</title>
</head>
<body>
<form name="frmLogin" method="POST" action="https://localhost:8080/Web1/LoginServlet">
<table>
<tr>
<td>Username</td>
<td><input type="text" name="username"></td>
</tr>
<tr>
<td>Password</td>
<td><input type="password" name="password"></td>
</tr>
<tr>
<td colspan="2"><button type="submit">Login</button></td>
</tr>
</table>
</form>
</body>
</html>
LoginServlet.java代码:
package com.journaldev.examples;
import java.io.IOException;
import java.sql.*;
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;
@WebServlet("/LoginServlet")
public class LoginServlet extends HttpServlet {
static {
try {
Class.forName("com.mysql.jdbc.Driver");
} catch (Exception e) {}
}
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
boolean success = false;
String username = request.getParameter("username");
String password = request.getParameter("password");
// Unsafe query which uses string concatenation
String query = "select * from tbluser where username='" + username + "' and password = '" + password + "'";
Connection conn = null;
Statement stmt = null;
try {
conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/user", "root", "root");
stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(query);
if (rs.next()) {
// Login Successful if match is found
success = true;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
stmt.close();
conn.close();
} catch (Exception e) {}
}
if (success) {
response.sendRedirect("home.html");
} else {
response.sendRedirect("login.html?error=1");
}
}
}
数据库查询[MySQL]:
create database user;
create table tbluser(username varchar(32) primary key, password varchar(32));
insert into tbluser (username,password) values ('john','secret');
insert into tbluser (username,password) values ('mike','pass10');
当在登录页面输入有效的用户名和密码时。
請輸入用戶名稱:約翰
输入用户名:秘密
查询:选择tbluser表中所有信息,其中username为‘john’且password为‘secret’。
结果:用户名和密码在数据库中存在,因此身份验证成功。用户将被重定向到主页。
2. 利用 SQL 注入未经授权地访问系统
输入用户名:dummy
输入密码:’ 或 ‘1’=’1’。
查询:从tbluser表中选择所有数据,其中用户名为’dummy’且密码为空或者’1’=’1’。
结果:输入的用户名和密码在数据库中不存在,但身份验证成功。为什么?
因为我们输入了 ’ or ‘1’=’1 作为密码,所以出现了SQL注入问题。查询条件有三种情况。
- 用户名为’dummy’:由于表中没有用户名为dummy的用户,它将被评估为false。
密码为”:由于表中没有空密码,它将被评估为false。
‘1’=’1’:由于这是静态字符串比较,它将被评估为true。
现在将所有3个条件进行组合,即假和假或真=> 最终结果将为真。
在上述情况下,我们使用布尔表达式来进行SQL注入。还有其他一些方法可以进行SQL注入。在下一部分中,我们将看到如何在我们的Java应用程序中预防SQL注入。
在Java代码中防止SQL注入
使用预编译语句(PreparedStatement)而不是语句(Statement)来执行查询是最简单的解决方案。
我们通过PreparedStatement的setter方法将用户名和密码提供给查询,而不是将它们连接成查询语句。
现在,从请求中接收到的用户名和密码的值被视为仅仅是数据,因此不会发生SQL注入。
让我们来看一下修改后的servlet代码。
String query = "select * from tbluser where username=? and password = ?";
Connection conn = null;
PreparedStatement stmt = null;
try {
conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/user", "root", "root");
stmt = conn.prepareStatement(query);
stmt.setString(1, username);
stmt.setString(2, password);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
// Login Successful if match is found
success = true;
}
rs.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
stmt.close();
conn.close();
} catch (Exception e) {
}
}
让我们了解一下这个案例中正在发生的事情。
查询:从tbluser表中选择*,其中username =?,password =?
以上查询中的问号(?)被称为位置参数。以上查询中有2个位置参数。我们不将用户名和密码连接到查询中。我们使用PreparedStatement中提供的方法来提供用户输入。
我们使用stmt.setString(1, username)将第一个参数设置为用户名,并使用stmt.setString(2, password)将第二个参数设置为密码。底层的JDBC API负责对这些值进行处理,以避免SQL注入的情况发生。
避免SQL注入的最佳实践
- 在查询之前验证数据的有效性。
不要将常用词作为表名或列名。例如,许多应用程序使用tbluser或tblaccount来存储用户数据。email,firstname,lastname是常见的列名。
不要直接将用户输入的数据连接起来创建SQL查询。
在应用程序的数据层中使用Hibernate和Spring Data JPA等框架。
在查询中使用位置参数。如果使用普通的JDBC,则使用PreparedStatement来执行查询。
通过权限和授权限制应用程序对数据库的访问。
不要将敏感的错误代码和信息返回给最终用户。
进行适当的代码审查,以确保没有开发人员意外编写不安全的SQL代码。
使用像SQLMap这样的工具来查找和修复应用程序中的SQL注入漏洞。
Java SQL注入的内容就是这些了,我希望没有遗漏任何重要的内容。
您可以从以下链接下载示例的Java Web应用程序项目。
SQL注入的Java项目