SQL注入与防御措施
1. SQL注入概述
SQL注入是一种通过将恶意SQL代码插入到输入字段中来操纵数据库查询的攻击方式。攻击者可以通过SQL注入修改查询的逻辑,执行未授权的操作,如读取敏感数据、篡改数据库、甚至删除数据。
2. SQL语句的闭合问题
SQL注入攻击中常见的手段之一是破坏SQL语句的结构,利用不正确的字符闭合来执行额外的SQL代码。然而,并不是所有类型的SQL注入都依赖于闭合字符,这种情况只适用于本身进行sql查询的时候就没有使用字符去进行闭合。以下是两种不同的注入方式:
-
传统注入::
SELECT * FROM users WHERE username = 'admin' OR 1=1 -- ';
在这里,输入
admin' OR 1=1 --
成功关闭了原始查询,并通过OR 1=1
使查询总是为真。 -
基于时间的盲注:
SELECT * FROM users WHERE id = 1 AND IF(SLEEP(5), 1, 0);
3. 特殊字符的转义与猜测闭合的防御
理论上来说,使用各种特殊字符组合来混淆闭合方式可能会增加攻击者的难度,但这并不是一个可靠的防御策略。真正有效的防御方法是通过参数化查询或预编译语句来确保用户输入无法破坏SQL语句结构。
4. PHP中的魔术引号(Magic Quotes)机制
PHP早期版本中引入了魔术引号(magic_quotes_gpc
)机制,自动转义用户输入的特殊字符,如单引号和双引号,目的是防止SQL注入。然而,这种机制存在局限性:
-
无法处理复杂注入:魔术引号只能处理简单的字符串输入,不能防御基于数值或逻辑的注入。
-
不可靠:魔术引号在某些情况下引发兼容性问题,并且不同数据库处理转义的方式不同。
由于其缺陷,PHP 5.3.0 开始废弃了魔术引号,PHP 5.4.0 后彻底移除。现在推荐的做法是使用预编译语句。
而且会导致sql二次注入。
5. 参数化查询
参数化查询是防止SQL注入的最有效方法。它们将SQL语句与用户输入分离,确保输入只作为数据处理,而不会影响SQL语句的结构。
参数化查询
-
是一种查询构建方式,它将 SQL 语句中的可变部分(通常是用户输入)用参数来代替。这些参数在 SQL 语句被预编译后才传入。例如,在 PHP 的 PDO 中,使用
bindParam
方法或者在 Java 的PreparedStatement
中使用setString
等方法来设置参数。参数化查询的主要目的是将用户输入的数据和 SQL 语句的逻辑部分分开,防止 SQL 注入。 -
更强调查询语句的构建方式,即如何将用户输入以安全的参数形式融入到 SQL 语句中。其核心是通过参数来隔离用户输入和 SQL 语法部分,确保用户输入被当作数据而不是 SQL 语句的一部分来处理,从而保障安全性。
预编译
-
是一种数据库操作机制。当使用预编译语句时,数据库会先对 SQL 语句的结构进行解析和编译,生成一个执行计划。例如,在 Java 的 JDBC 中,
PreparedStatement
对象就是预编译后的产物。像connection.prepareStatement(query)
这个操作,会把 SQL 语句发送给数据库进行预编译。 -
它主要关注的是 SQL 语句的编译过程,使得数据库能够提前准备好执行计划,提高查询的执行效率。特别是对于重复执行的相似查询(只是参数不同),预编译可以让数据库复用之前的编译结果,减少编译时间。
虽然预编译通常用于实现参数化查询,但参数化查询的概念也可以通过其他方式来近似实现,不过安全性和效率可能不如预编译。例如,在一些简单的数据库操作库中,可能会通过手动拼接 SQL 语句并进行严格的输入过滤来模拟参数化查询的安全性。但这种方式没有真正利用数据库的预编译机制,可能无法达到预编译带来的性能提升,并且过滤用户输入的过程也可能存在漏洞。
PHP 参数化查询(使用 PDO):
-
连接数据库并准备查询语句
-
首先,使用 PDO(PHP Data Objects)建立与数据库的连接。PDO 是一个统一的 API,可以用于多种不同的数据库系统(如 MySQL、PostgreSQL 等)。以下是一个连接 MySQL 数据库的示例:
-
try { $pdo = new PDO('mysql:host=localhost;dbname=your_database_name', 'your_username', 'your_password'); $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); // 准备查询语句,使用命名参数(:username和:password) $query = "SELECT * FROM users WHERE username = :username AND password = :password"; $stmt = $pdo->prepare($query); } catch (PDOException $e) { echo "数据库连接错误: ". $e->getMessage(); }
-
绑定参数并执行查询
-
假设用户输入的用户名和密码分别存储在
$input_username
和$input_password
变量中,将这些参数绑定到准备好的查询语句中并执行:
-
$input_username = "user1"; $input_password = "password1"; // 绑定参数 $stmt->bindParam(':username', $input_username, PDO::PARAM_STR); $stmt->bindParam(':password', $input_password, PDO::PARAM_STR); // 执行查询 $stmt->execute(); // 获取查询结果 $result = $stmt->fetchAll(PDO::FETCH_ASSOC); if (count($result) > 0) { echo "登录成功"; } else { echo "登录失败"; }
-
在这个例子中,通过
bindParam
方法将用户输入的变量绑定到查询语句中的命名参数(:username
和:password
)上。PDO::PARAM_STR
表示参数的数据类型是字符串。这样,用户输入就被当作纯数据处理,有效地防止了 SQL 注入。
Java 参数化查询(使用 JDBC)
-
连接数据库并准备查询语句
-
首先,使用 JDBC(Java Database Connectivity)建立与数据库的连接。以下是一个连接 MySQL 数据库的示例(需要导入相应的 JDBC 驱动包,如
mysql - connector - java
):
-
import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; public class LoginExample { public static void main(String[] args) { Connection connection = null; PreparedStatement preparedStatement = null; ResultSet resultSet = null; try { // 建立与MySQL数据库的连接 connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/your_database_name", "your_username", "your_password"); // 准备查询语句,使用问号(?)作为参数占位符 String query = "SELECT * FROM users WHERE username =? AND password =?"; preparedStatement = connection.prepareStatement(query); } catch (SQLException e) { System.out.println("数据库连接错误: " + e.getMessage()); } } }
-
设置参数并执行查询
-
假设用户输入的用户名和密码分别存储在
userInputUsername
和userInputPassword
变量中,将这些参数设置到准备好的查询语句中并执行:
-
String userInputUsername = "user1"; String userInputPassword = "password1"; try { // 设置参数,索引从1开始 preparedStatement.setString(1, userInputUsername); preparedStatement.setString(2, userInputPassword); // 执行查询 resultSet = preparedStatement.executeQuery(); if (resultSet.next()) { System.out.println("登录成功"); } else { System.out.println("登录失败"); } } catch (SQLException e) { System.out.println("查询错误: " + e.getMessage()); } finally { // 关闭资源 try { if (resultSet!= null) resultSet.close(); if (preparedStatement!= null) preparedStatement.close(); if (connection!= null) connection.close(); } catch (SQLException e) { System.out.println("资源关闭错误: " + e.getMessage()); } }
-
在 Java 的例子中,通过
setString
(根据参数类型还有setInt
、setDouble
等方法)方法将用户输入的值设置到PreparedStatement
对象中的参数位置上(参数索引从 1 开始)。这种方式同样将用户输入与 SQL 语句的逻辑部分分开,防止 SQL 注入攻击。
JAVA持久层框架预编译
持久层主要负责将数据在内存中的对象和数据库中的记录进行相互转换和持久化存储。
MyBatis
-
MyBatis 是一款优秀的持久层框架,用于简化 Java 应用程序与各种数据库之间的交互操作。它可以将 SQL 语句与 Java 代码进行解耦,使得开发人员能够更加灵活、高效地进行数据库访问。
-
在企业级 Java 开发中,数据持久化是一个关键环节。MyBatis 通过提供一种简洁而强大的方式来处理数据库操作,避免了开发人员直接编写复杂的 JDBC(Java Database Connectivity)代码,减少了大量的样板代码,并且提高了代码的可维护性和可读性。
MyBatis 的主要特点
-
SQL 与代码分离
-
MyBatis 允许将 SQL 语句写在独立的 XML 配置文件或者使用 Java 注解的方式嵌入到 Java 代码中。以 XML 配置文件为例,开发人员可以在其中编写各种 SQL 查询、插入、更新和删除语句,如:
-
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.dao.UserDao"> <select id="selectUserById" resultMap="UserResultMap"> SELECT * FROM users WHERE id = # {id} </select> </mapper>
-
这样,SQL 语句和 Java 业务逻辑代码分开编写,使得代码结构更加清晰。当 SQL 语句需要修改时,不需要在 Java 代码中进行复杂的查找和修改,直接在 XML 文件中调整即可。
MyBatis 在项目中的应用场景
-
数据库访问层开发
-
在多层架构的 Java 应用程序中,MyBatis 主要应用于数据访问层(DAO - Data Access Object)。开发人员可以通过定义接口和对应的 XML 配置文件或者使用注解来实现数据库操作方法。例如,定义一个
UserDao
接口用于操作用户表:
-
public interface UserDao { User selectUserById(int id); List<User> selectUsersByCondition(UserCondition condition); void insertUser(User user); void updateUser(User user); void deleteUserById(int id); }
-
然后通过 MyBatis 的配置和映射机制来实现这些接口方法对应的数据库操作,为上层业务逻辑提供数据访问服务。
MyBatis 中$
和#
的处理方式差异
-
#
的处理(预编译)-
当你使用
#
来引用参数时,MyBatis 会将其视为一个预编译的参数。例如,在 SQL 语句SELECT * FROM users WHERE id = # {id}
中,id
这个参数会被 MyBatis 处理为一个安全的预编译参数。具体来说,MyBatis 会将这个参数进行安全地处理,采用预编译的方式将其发送给数据库。它会把参数部分当作一个数据值,而不是 SQL 语句的一部分,不管参数内容如何,都不会改变 SQL 语句的逻辑结构。这是一种安全的参数传递方式,可以有效地防止 SQL 注入。
-
-
$
的处理(直接拼接)-
当使用
$
来引用参数时,MyBatis 会将其内容直接拼接到 SQL 语句中。例如,在 SQL 语句SELECT * FROM users WHERE name = $ {name}
中,name
这个变量的值会直接拼接到 SQL 语句中。这种方式没有采用预编译来隔离用户输入和 SQL 语句逻辑,所以如果用户输入的name
值包含 SQL 关键字或者特殊的 SQL 语法结构,就很容易引发 SQL 注入攻击。
-
Hibernate
-
Hibernate 是一个功能强大的对象 - 关系映射(ORM)框架,它也用于 Java 应用程序与数据库之间的交互。与 MyBatis 不同的是,Hibernate 更强调面向对象的编程方式,它允许开发者以操作 Java 对象的方式来间接操作数据库。例如,通过定义实体类(Entity Class)和对应的映射关系,就可以使用诸如
session.save()
、session.get()
等方法来实现数据的保存和查询,而不需要编写大量的 SQL 语句。 -
在 Hibernate 中,主要是通过 Hibernate Query Language(HQL)或 Criteria API 来构建查询。HQL 在语法上类似于 SQL,但它是面向对象的,操作的是实体类和它们的属性。在参数处理方面,Hibernate 会自动进行安全的参数绑定,类似于预编译的方式,将参数视为数据,而不是 SQL 语句的一部分。例如,在 HQL 语句
from User u where u.username = :username
中,username
这个参数会被安全地处理,Hibernate 会自动处理参数绑定过程,防止 SQL 注入。
JPA
-
JPA 是 Java EE 规范中的一部分,它提供了一种标准的方式来进行对象 - 关系映射。JPA 本身是一个接口规范,有多种实现框架,如 EclipseLink 和 Hibernate(Hibernate 也是 JPA 的一种实现)。JPA 通过
EntityManager
接口来执行各种数据库操作,使用 JPQL(Java Persistence Query Language)来构建查询。 -
JPQL 在语法和功能上与 HQL 类似,也是面向对象的查询语言。在参数处理上,JPA 同样会采用安全的参数绑定方式,类似于预编译。例如,在 JPQL 语句
SELECT u FROM User u WHERE u.age > :minAge
中,minAge
这个参数会被正确地绑定,确保安全地传递到数据库执行,避免 SQL 注入。
7. 输入作为数据与作为SQL语句的区别
-
作为数据:当输入作为SQL查询的参数传递时,它被当作一个值处理。即使输入中包含SQL代码(如
OR 1=1
),它也仅仅是一个字符串或数值,数据库不会将其解释为SQL命令。 -
作为SQL语句:当用户输入直接拼接到SQL查询中时,输入中的SQL代码可能改变查询的结构,导致注入。例如:
SELECT * FROM users WHERE username = 'admin' OR 1=1 -- ';
在这个例子中,
OR 1=1
被解释为SQL语法,改变了查询逻辑。
8. 预编译语句中的闭合问题
在使用预编译语句时,SQL引擎会将用户输入作为数据处理,而不会将其解释为SQL语句的一部分。因此,即使不做手动转义,输入中的引号和特殊字符也不会导致查询结构的破坏或闭合问题。
示例:
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :username"); $stmt->bindParam(':username', $input); $stmt->execute();
即使用户输入 admin' OR 1=1 --
,SQL引擎仍会将其视为纯字符串处理,不会执行注入。
9. 总结
-
预编译语句和参数化查询是防止SQL注入的最有效方法,通过将SQL结构与用户输入分离,保证输入不会影响SQL的执行逻辑。
-
魔术引号已被PHP废弃,不能依赖它来防御SQL注入。
-
输入作为数据与作为SQL语句的关键区别在于是否直接改变查询结构。通过参数化查询,输入不会改变查询结构,避免了注入风险。
原文地址:https://blog.csdn.net/m0_48294281/article/details/144372210
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!