深入理解 SQL 注入漏洞及解决方案

发布于:2025-02-24 ⋅ 阅读:(15) ⋅ 点赞:(0)

一、引言

在当今数字化时代,数据库作为存储和管理数据的核心组件,其安全性至关重要。SQL 注入是一种常见且极具威胁性的数据库安全漏洞,它可能导致数据泄露、篡改甚至系统被完全控制。本文将深入探讨 SQL 注入漏洞的产生原因、表现形式以及如何有效地解决这一问题。

二、SQL 注入漏洞的产生效果

在已知用户名的情况下,攻击者可以通过输入特殊构造的密码,利用 SQL 注入漏洞绕过系统的身份验证机制,成功登录系统。例如,原本需要正确的用户名和密码才能访问的系统,攻击者只需输入特定的字符组合,如 “aaa'or'1=1” 作为密码(用户名已知),就可以在不知道正确密码的情况下登录进去,这显然严重破坏了系统的安全性和访问控制机制。

三、SQL 注入漏洞的产生原因

SQL 注入漏洞的产生主要源于应用程序在拼接 SQL 语句时没有对用户输入进行充分的验证和过滤。以常见的登录场景为例,后台程序拼接 SQL 语句的代码通常如下:

String sql = "select * from t_user where username = '"+username+"' and password = '"+password+"'";

在这种情况下,如果用户输入恶意的字符,就会改变 SQL 语句的逻辑。比如,当用户输入 “aaa'or'1=1” 作为密码时,拼接后的 SQL 语句变为:

String sql = "select * from t_user where username = 'aaa'or'1=1' and password = 'sfsdfsds";

由于 “or '1=1'” 恒为真,无论密码是否正确,该查询都能返回结果,从而导致攻击者绕过了密码验证。

按照 AND 优先级高于 OR 的规则,它会先计算 '1=1' and password = '123456' 这部分,然后再与前面的 username = 'aaa' 通过 OR 进行运算。

如果 '1=1' 恒为 true,当 password = '123456' 结果为 false 时,'1=1' and password = '123456' 结果为 false ,但由于前面有 OR 连接 username = 'aaa' ,只要 username = 'aaa' 为 true ,整个表达式的结果就为 true ,这就可能导致不符合预期的查询结果,也是 SQL 注入利用的一个原理基础。

另一种常见的恶意输入是 “aaa'‐‐ '”,拼接后的 SQL 语句变为:

String sql = "select * from t_user where username = 'aaa'‐‐ '' and password = 'sfsdfsdfs";

“‐‐” 在 SQL 中是注释符,这使得后面的密码验证部分被注释掉,查询只验证用户名,同样也实现了非法登录。

四、SQL 注入漏洞的解决方案

为了解决 SQL 注入漏洞,我们可以使用PreparedStatement接口,它是Statement的子接口,具有以下优势:

  1. 预编译功能PreparedStatement能够将 SQL 语句中的参数部分使用 “?”(占位符)来代替,并先将编写的 SQL 语句发送到 MySQL 服务器端进行编译。编译后的 SQL 语句格式固定,后续传入的任何值都会作为 “?” 的参数处理,从而避免了恶意输入改变 SQL 语句逻辑的情况。
  2. 具体使用方法
    • 获取预编译对象:通过Connection接口的prepareStatement(String sql)方法来预编译 SQL 语句。例如:
Connection conn = JdbcUtils.getConnection();
String sql = "select * from y_user where username =? and password =?";
PreparedStatement stmt = conn.prepareStatement(sql);
  • 设置参数值:使用setXxxx()系列方法(如setString()setInt()等)向 “?” 传入值,参数的位置从 1 开始计数。例如:
stmt.setString(1,username);
stmt.setString(2,password);
  • 执行 SQL 语句:根据 SQL 语句的类型,使用executeQuery()方法执行查询操作(返回结果集),使用executeUpdate()方法执行增删改操作。例如:
ResultSet rs = stmt.executeQuery();

五、示例代码分析

以下是一个完整的 Java 代码示例,展示了如何模拟 SQL 注入漏洞以及如何使用PreparedStatement解决该问题:

package cn.qcby.demo1;

import cn.qcby.utils.JdbcUtils;

import java.sql.*;

/**
 * 演示SQL注入的问题,漏洞
 * 在已知用户名的情况下,通过sql语言关键字,登录系统。密码随意输入的。
 * SQL注入产生原因是SQL语句的拼接,利用SQL关键字产生效果。
 * 需要解决SQL注入的问题
 *
 * 解决SQL注入问题,采用SQL语句预编译的方式,把SQL语句中的参数使用?占位符来表示,先把SQL语句编译,格式固定的。
 * 再给?传入值,传入任何内容都表示值。数据库会判断SQL执行的结果。
 */
public class JdbcTest4 {

    public static void main(String[] args) {

        // 模拟登录的功能,有SQL注入的问题
        //String result = new JdbcTest4().login("aaa'or'1=1", "1234sdf");
        //System.out.println(result);

        String result = new JdbcTest4().login2("aaa'or'1=1", "123456");
        System.out.println(result);
    }


    /**
     * 采用预编译的方式,解决SQL注入的问题
     * @param username
     * @param password
     * @return
     */
    public String login2(String username,String password){
        Connection conn = null;
        // 预编译执行SQL语句对象
        PreparedStatement stmt = null;
        ResultSet rs = null;
        try {
            // 获取到连接
            conn = JdbcUtils.getConnection();
            // 使用?占位符
            String sql = "select * from y_user where username =? and password =?";
            // 预编译SQL语句,把SQL语句固定
            //Statement statement = conn.createStatement();
            //statement不能防止sql注入问题    prepareStatement 能够防止sql注入问题
            stmt = conn.prepareStatement(sql);
            // 需要给?设置值
            stmt.setString(1,username);
            stmt.setString(2,password);
            // 执行SQL语句
            rs = stmt.executeQuery();
            // 遍历数据
            if(rs.next()){
                // 表示存在数据,如果存在,说明用户名和密码编写正确
                return "登录成功...";
            }else{
                return "登录失败了...";
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }finally {
            JdbcUtils.close(conn,stmt,rs);
        }
        return null;
    }

    /**
     * 模拟登录的功能,通过用户名和密码从数据库中查询
     * @param username
     * @param password
     * @return
     */
    public String login(String username,String password){
        Connection conn = null;
        Statement stmt = null;
        ResultSet rs = null;
        try {
            // 获取到连接
            conn = JdbcUtils.getConnection();
            // 编写SQL语句的拼接  '1=1' true  password = '1234sdfsce' false  true and false  整体上false
            // String sql = "select * from t_user where username = 'aaa' or '1=1' and password = '1234sdfsce'";
            // String sql = "select * from t_user where username = 'aaa' or false";
            String sql = "select * from y_user where username = '"+username+"' and password = '"+password+"'";
            // 执行sql
            stmt = conn.createStatement();
            // 执行
            rs = stmt.executeQuery(sql);
            // 遍历数据
            if(rs.next()){
                // 表示存在数据,如果存在,说明用户名和密码编写正确
                return "登录成功...";
            }else{
                return "登录失败了...";
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }finally {
            JdbcUtils.close(conn,stmt,rs);
        }
        return null;
    }

}