【JDBC】如何保护 JDBC 应用程序免受 SQL 注入的影响

本文仅供学习参考!
相关教程地址:
https://zhuanlan.zhihu.com/p/397815893
https://www.freebuf.com/articles/web/339118.html
https://www.developer.com/design/how-to-protect-a-jdbc-application-against-sql-injection/

在这里插入图片描述

概述

在关系数据库管理系统 (RDBMS) 中,有一种特定的语言称为 SQL(结构化查询语言),用于与数据库进行通信。用 SQL 编写的查询语句用于操作数据库的内容和结构。创建和修改数据库结构的特定 SQL 语句称为 DDL(数据定义语言)语句,操作数据库内容的语句称为 DML(数据操作语言)语句。与 RDBMS 包关联的引擎解析和解释 SQL 语句,并相应地返回结果。这是与RDBMS通信的典型过程 - 触发SQL语句并返回结果,仅此而已。系统不会判断任何符合语言语法和语义结构的语句的意图。这也意味着没有身份验证或验证过程来检查谁触发了语句以及获取输出的权限。攻击者可以简单地恶意触发 SQL 语句,并获取它不应该获取的信息。例如,攻击者可以使用看似无害的查询执行具有恶意负载的 SQL 语句,以控制 Web 应用程序的数据库服务器。

工作原理

攻击者可以利用此漏洞并利用它来为自己谋取利益。例如,可以绕过应用程序的身份验证和授权机制,从整个数据库中检索所谓的安全内容。SQL 注入可用于从数据库中创建、更新和删除记录。因此,人们可以使用SQL制定一个仅限于自己想象的查询。

通常,应用程序经常出于多种目的向数据库触发 SQL 查询,无论是提取某些记录、创建报告、对用户进行身份验证、CRUD 事务等。攻击者只需在某个应用程序输入表单中找到 SQL 输入查询。然后,表单准备的查询可用于缠绕恶意内容,以便在应用程序触发查询时,它也携带注入的有效负载。

理想的情况之一是应用程序要求用户输入用户名或用户 ID。该应用程序在那里打开了一个脆弱的地方。SQL 语句可以在不知不觉中运行。攻击者通过注入要用作 SQL 查询的一部分并由数据库处理的有效负载来利用此漏洞。例如,登录表单的 POST 操作的服务器端伪代码可能是:

uname = getRequestString("username");
pass = getRequestString("passwd");

stmtSQL = "SELECT * FROM users WHERE
   user_name = '" + uname + "' AND passwd = '" + pass + "'";

database.execute(stmtSQL);

前面的代码容易受到 SQL 注入攻击,因为通过变量 ‘uname’ 和 ‘pass’ 提供给 SQL 语句的输入可以以一种改变语句语义的方式进行操作。

例如,我们可以修改查询以针对数据库服务器运行,就像在 MySQL 中一样。

stmtSQL = "SELECT * FROM users WHERE
   user_name = '" + uname + "' AND passwd = '" + pass + "' OR 1=1";

这会导致将原始 SQL 语句修改到能够绕过身份验证的程度。这是一个严重的漏洞,必须从代码中防止。

防御 SQL 注入攻击

减少 SQL 注入攻击几率的方法之一是确保在执行之前不允许将未过滤的文本字符串附加到 SQL 语句中。例如,我们可以使用 PreparedStatement 来执行所需的数据库任务。PreparedStatement 的有趣之处在于它将预编译的 SQL 语句发送到数据库,而不是字符串。这意味着查询和数据将分别发送到数据库。这可以防止SQL注入攻击的根本原因,因为在SQL注入中,这个想法是混合代码和数据,其中数据实际上是数据伪装的代码的一部分。在 PreparedStatement 中,有多个 setXYZ() 方法,例如 *setString()。*这些方法用于过滤特殊字符,例如 SQL 语句中包含的引号。

例如,我们可以通过以下方式执行 SQL 语句。

String sql = "SELECT * FROM employees WHERE emp_no = "+eno;

我们可以修改查询,而不是在输入中输入 eno=10125 作为员工编号,例如:

eno = 10125 OR 1=1

这将完全更改查询返回的结果。

一个例子

在下面的示例代码中,我们展示了如何使用 PreparedStatement 来执行数据库任务。

package org.mano.example;

import java.sql.*;
import java.time.LocalDate;
public class App
{
    
    
   static final String JDBC_DRIVER =
      "com.mysql.cj.jdbc.Driver";
   static final String DB_URL =
      "jdbc:mysql://localhost:3306/employees";
   static final String USER = "root";
   static final String PASS = "secret";
   public static void main( String[] args )
   {
    
    
      String selectQuery = "SELECT * FROM employees
         WHERE emp_no = ?";
      String insertQuery = "INSERT INTO employees
         VALUES (?,?,?,?,?,?)";
      String deleteQuery = "DELETE FROM employees
         WHERE emp_no = ?";
      Connection connection = null;
      try {
    
    
         Class.forName(JDBC_DRIVER);
         connection = DriverManager.getConnection
            (DB_URL, USER, PASS);
      }catch(Exception ex) {
    
    
         ex.printStackTrace();
      }
      try(PreparedStatement pstmt =
            connection.prepareStatement(insertQuery);){
    
    
         pstmt.setInt(1,99);
         pstmt.setDate(2, Date.valueOf
            (LocalDate.of(1975,12,11)));
         pstmt.setString(3,"ABC");
         pstmt.setString(4,"XYZ");
         pstmt.setString(5,"M");
         pstmt.setDate(6,Date.valueOf(LocalDate.of(2011,1,1)));
         pstmt.executeUpdate();
         System.out.println("Record inserted successfully.");
      }catch(SQLException ex){
    
    
         ex.printStackTrace();
      }
      try(PreparedStatement pstmt =
            connection.prepareStatement(selectQuery);){
    
    
         pstmt.setInt(1,99);
         ResultSet rs = pstmt.executeQuery();
         while(rs.next()){
    
    
            System.out.println(rs.getString(3)+
               " "+rs.getString(4));
         }
      }catch(Exception ex){
    
    
         ex.printStackTrace();
      }
      try(PreparedStatement pstmt =
            connection.prepareStatement(deleteQuery);){
    
    
         pstmt.setInt(1,99);
         pstmt.executeUpdate();
         System.out.println("Record deleted
            successfully.");
      }catch(SQLException ex){
    
    
         ex.printStackTrace();
      }
      try{
    
    
         connection.close();
      }catch(Exception ex){
    
    
         ex.printStackTrace();
      }
   }
}

一窥PreparedStatement

这些作业也可以使用 JDBC 语句接口完成,但问题是它有时可能非常不安全,尤其是当执行动态 SQL 语句来查询用户输入值与 SQL 查询连接的数据库时。正如我们所看到的,这可能是一个危险的情况。在大多数普通情况下,Statement是无害的,但PreparedStatement似乎是两者之间更好的选择。它可以防止恶意字符串被连接,因为它在将语句发送到数据库时采用了不同的方法。PreparedStatement 使用变量替换而不是串联。在 SQL 查询中放置问号 (?) 表示替换变量将取代它并在执行查询时提供值。替换变量的位置根据 setXYZ() 方法中分配的参数索引位置取而代之。

此技术可防止其受到 SQL 注入攻击。

此外,PreparedStatement 实现了 AutoCloseable。这使它能够在“试用资源”块的上下文中写入,并在超出范围时自动关闭。

结论

只有通过负责任地编写代码才能防止 SQL 注入攻击。事实上,在任何软件解决方案中,安全性大多是由于错误的编码实践而被破坏的。在这里,我们描述了要避免的内容以及 PreparedStatement 如何帮助我们编写安全代码。有关SQL注入的完整概念,请参阅适当的材料;互联网上到处都是它们,对于 PreparedStatement,请查看 Java API 文档以获得更详细的解释。

猜你喜欢

转载自blog.csdn.net/m0_47015897/article/details/131418868