一、引言
在当今数字化时代,数据库作为存储和管理数据的核心组件,其安全性至关重要。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
的子接口,具有以下优势:
- 预编译功能:
PreparedStatement
能够将 SQL 语句中的参数部分使用 “?”(占位符)来代替,并先将编写的 SQL 语句发送到 MySQL 服务器端进行编译。编译后的 SQL 语句格式固定,后续传入的任何值都会作为 “?” 的参数处理,从而避免了恶意输入改变 SQL 语句逻辑的情况。 - 具体使用方法:
- 获取预编译对象:通过
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+"'";// 执行sqlstmt = 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;}}