如何保护 JDBC 应用程序免受 SQL 注入

概述

在关系数据库管理系统 (RDBMS) 中,有一种特定的语言——称为 SQL(结构化查询语言)——用于与数据库进行通信。用 SQL 编写的查询语句用于操作数据库的内容和结构。创建和修改数据库结构的特定 SQL 语句称为 DDL(数据定义语言)语句,操作数据库内容的语句称为 DML(数据操作语言)语句。与 RDBMS 包关联的引擎解析和解释 SQL 语句并相应地返回结果。这是与 RDBMS 通信的典型过程——触发一条 SQL 语句并返回结果,仅此而已。系统不会判断任何符合语言语法和语义结构的语句的意图。这也意味着没有身份验证或验证过程来检查谁触发了该语句以及获取输出的特权。攻击者可以简单地触发带有恶意意图的 SQL 语句并取回它不应该获取的信息。例如,攻击者可以使用看似无害的查询执行带有恶意负载的 SQL 语句,以控制 Web 应用程序的数据库服务器。攻击者可以简单地触发带有恶意意图的 SQL 语句并取回它不应该获取的信息。例如,攻击者可以使用看似无害的查询执行带有恶意负载的 SQL 语句,以控制 Web 应用程序的数据库服务器。攻击者可以简单地触发带有恶意意图的 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似乎是两者之间更好的选择。由于其将语句发送到数据库的方法不同,它可以防止恶意字符串被连接。准备好的语句使用变量替换而不是连接。在 SQL 查询中放置问号 (?) 表示在执行查询时替换变量将代替它并提供值。替换变量的位置根据setXYZ()方法中分配的参数索引位置来代替。

这种技术可以防止它受到 SQL 注入攻击。

此外,PreparedStatement实现了 AutoCloseable。这使它能够在try-with-resources块的上下文中写入,并在超出范围时自动关闭。

结论

只有负责任地编写代码,才能防止 SQL 注入攻击。事实上,在任何软件解决方案中,安全性大多是由于糟糕的编码实践而被破坏的。在这里,我们描述了要避免什么以及PreparedStatement如何帮助我们编写安全代码。有关 SQL 注入的完整概念,请参阅相应的资料;Internet 上到处都是,对于PreparedStatement,请查看 Java API 文档以获取更详细的说明。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值