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项目