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

一、引言

在当今数字化时代,数据库作为存储和管理数据的核心组件,其安全性至关重要。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解决该问题:

java 复制代码
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;
    }

}
相关推荐
月光水岸New1 小时前
Ubuntu 中建的mysql数据库使用Navicat for MySQL连接不上
数据库·mysql·ubuntu
狄加山6751 小时前
数据库基础1
数据库
我爱松子鱼1 小时前
mysql之规则优化器RBO
数据库·mysql
chengooooooo1 小时前
苍穹外卖day8 地址上传 用户下单 订单支付
java·服务器·数据库
Rverdoser2 小时前
【SQL】多表查询案例
数据库·sql
Galeoto3 小时前
how to export a table in sqlite, and import into another
数据库·sqlite
人间打气筒(Ada)3 小时前
MySQL主从架构
服务器·数据库·mysql
leegong231113 小时前
学习PostgreSQL专家认证
数据库·学习·postgresql
喝醉酒的小白3 小时前
PostgreSQL:更新字段慢
数据库·postgresql
敲敲敲-敲代码3 小时前
【SQL实验】触发器
数据库·笔记·sql