Java中的SQL注入以及如何轻松防止它
什么是SQL注入?
SQL注入是十大Web应用程序漏洞之一。
简而言之,SQL注入意味着通过用户输入的数据在查询中注入/插入SQL代码。
它可以在使用关系数据库的任何应用程序中发生,例如Oracle,MySQL,PostgreSQL和SQL Server。
为了执行SQL注入,恶意用户首先尝试在应用程序中找到一个可以将SQL代码与数据一起嵌入的位置。
它可以是任何Web应用程序或者任何其他地方的登录页面。
因此,当应用程序接收到嵌入有SQL代码的数据时,SQL代码将与应用程序查询一起执行。
SQL注入的影响
恶意用户可以未经授权访问您的应用程序并窃取数据。
他们可以更改,删除数据库中的数据并关闭您的应用程序。
黑客还可以通过执行数据库特定的系统命令来控制运行数据库服务器的系统。
SQL注入如何工作?
假设我们有一个名为tbluser的数据库表,用于存储应用程序用户的数据。
userId是表的主列。
我们在应用程序中具有功能,使您可以通过userId获取信息。
从用户请求中接收到userId的值。
让我们看下面的示例代码。
String userId = {get data from end user}; String sqlQuery = "select * from tbluser where userId = " + userId;
1.有效的用户输入
当使用有效数据(即userId值132)执行上述查询时,其外观如下所示。
输入数据:132
执行的查询:从tbluser中选择*,其中userId = 132
结果:查询将返回具有userId 132的用户的数据。
在这种情况下,没有SQL注入发生。
2.黑客用户输入
黑客可以使用Postman,cURL等工具更改用户请求,以将SQL代码作为数据发送,并以此方式绕过任何UI端验证。
输入数据:2或者1 = 1
执行查询:从tbluser中选择*,其中userId = 2或者1 = 1
结果:现在上面的查询具有两个带有SQL OR表达式的条件。
userId = 2:这部分将匹配userId值为" 2"的表行。
1 = 1:这部分将始终被评估为true。
因此查询将返回表的所有行。
SQL注入的类型
让我们看一下SQL注入的四种类型。
1.基于布尔的SQL注入
上面的示例是基于布尔值的SQL注入的情况。
它使用布尔表达式,其结果为true或者false。
它可以用来从数据库中获取其他信息。
例如;
输入数据:2或者1 = 1
SQL查询:从tbl_employee中选择first_name,last_name,其中empId = 2或者1 = 1
2.基于联合的SQL注入
SQL联合运算符将来自两个不同查询的数据与相同数量的列组合在一起。
在这种情况下,并集运算符用于从其他表获取数据。
输入数据:2个联合选择用户名,tbluser的密码
查询:从tbl_employee中选择first_name,last_name,其中empId = 2 union选择用户名,来自tbluser的密码
通过使用基于联合的SQL注入,攻击者可以获得用户凭证。
3.基于时间的SQL注入
在基于时间的SQL注入中,特殊函数被注入查询中,可以在指定的时间内暂停执行。
这种攻击会降低数据库服务器的速度。
它可以通过影响数据库服务器性能来降低应用程序的性能。
例如,在MySQL中:
输入数据:2 + SLEEP(5)
查询:从tbl_employee中选择emp_id,first_name,last_name,其中empId = 2 + SLEEP(5)
在上面的示例中,查询执行将暂停5秒钟。
4.基于错误的SQL注入
在此变体中,攻击者试图从数据库中获取信息,例如错误代码和消息。
攻击者注入的SQL在语法上是错误的,因此数据库服务器将返回错误代码和消息,这些消息可用于获取数据库和系统信息。
Java SQL注入示例
我们将使用一个简单的Java Web应用程序来演示SQL注入。
我们有Login.html,这是一个基本的登录页面,该页面从用户那里获取用户名和密码并将其提交给LoginServlet。
LoginServlet从请求中获取用户名和密码,并根据数据库值对它们进行验证。
如果身份验证成功,则Servlet会将用户重定向到主页,否则它将返回错误。
Login.html代码:
<!DOCTYPE html> <html lang="en"> <head> <title>Sql Injection Demo</title> </head> <body> <form name="frmLogin" method="POST" action="https://localhost:8080/Web1/LoginServlet"> <table> <tr> <td>Username</td> <td><input type="text" name="username"></td> </tr> <tr> <td>Password</td> <td><input type="password" name="password"></td> </tr> <tr> <td colspan="2"><button type="submit">Login</button></td> </tr> </table> </form> </body> </html>
LoginServlet.java代码:
package com.theitroad.examples; import java.io.IOException; import java.sql.*; import javax.servlet.*; import javax.servlet.annotation.WebServlet; import javax.servlet.http.*; @WebServlet("/LoginServlet") public class LoginServlet extends HttpServlet { static { try { Class.forName("com.mysql.jdbc.Driver"); } catch (Exception e) {} } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { boolean success = false; String username = request.getParameter("username"); String password = request.getParameter("password"); //Unsafe query which uses string concatenation String query = "select * from tbluser where username='" + username + "' and password = '" + password + "'"; Connection conn = null; Statement stmt = null; try { conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/user", "root", "root"); stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(query); if (rs.next()) { //Login Successful if match is found success = true; } } catch (Exception e) { e.printStackTrace(); } finally { try { stmt.close(); conn.close(); } catch (Exception e) {} } if (success) { response.sendRedirect("home.html"); } else { response.sendRedirect("login.html?error=1"); } } }
数据库查询[MySQL]:
create database user; create table tbluser(username varchar(32) primary key, password varchar(32)); insert into tbluser (username,password) values ('john','secret'); insert into tbluser (username,password) values ('mike','pass10');
1.从登录页面输入有效用户名和密码时
输入用户名:john
输入用户名:秘密
查询:从tbluser中选择*,其中用户名=" john",密码="秘密"
结果:数据库中存在用户名和密码,因此身份验证成功。
用户将被重定向到主页。
2.使用SQL Injection获得未经授权的系统访问
输入用户名:哑
输入密码:"或者" 1 =" 1"
查询:从tbluser中选择*,其中username =" dummy",密码="或者" 1" =" 1"
结果:输入的用户名和密码在数据库中不存在,但身份验证成功。
为什么?
这是由于SQL注入,因为我们输入了"或者"或者" 1" =" 1"作为密码。
查询中有3个条件。
username ='dummy':因为表中没有用户名是虚拟的用户,它将被评估为false。
password ='':因为表中没有空密码,它将被评估为false。
1
=1
:因为这是静态字符串比较,它将被评估为true。
现在结合所有三个条件,即false和false或者true =>最终结果将为true。
在上述情况下,我们使用了布尔表达式来执行SQL注入。
还有其他一些方法可以执行SQL注入。
在下一节中,我们将介绍在Java应用程序中防止SQL注入的方法。
防止Java代码中的SQL注入
最简单的解决方案是使用PreparedStatement而不是Statement执行查询。
我们没有将用户名和密码连接到查询中,而是提供它们通过PreparedStatement的setter方法进行查询。
现在,从请求接收的用户名和密码的值仅被视为数据,因此不会发生SQL注入。
让我们看一下修改后的servlet代码。
String query = "select * from tbluser where username=? and password = ?"; Connection conn = null; PreparedStatement stmt = null; try { conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/user", "root", "root"); stmt = conn.prepareStatement(query); stmt.setString(1, username); stmt.setString(2, password); ResultSet rs = stmt.executeQuery(); if (rs.next()) { //Login Successful if match is found success = true; } rs.close(); } catch (Exception e) { e.printStackTrace(); } finally { try { stmt.close(); conn.close(); } catch (Exception e) { } }
让我们了解在这种情况下发生了什么。
查询:从tbluser中选择*,其中username =?和密码=?
以上查询中的问号(?)称为位置参数。
以上查询中有2个位置参数。
我们不会将用户名和密码连接在一起进行查询。
我们使用PreparedStatement中可用的方法来提供用户输入。
我们已经通过使用stmt.setString(1,username)
设置了第一个参数,并通过了stmt.setString(2,password)
设置了第二个参数。
底层JDBC API负责清理值以避免SQL注入。
避免SQL注入的最佳实践
在查询中使用数据之前,请先对其进行验证。
不要使用常用词作为表名或者列名。
例如,许多应用程序使用tbluser或者tblaccount存储用户数据。
电子邮件,名字,姓氏是常见的列名。不要直接串联数据(作为用户输入接收)来创建SQL查询。
将Hibernate和Spring Data JPA之类的框架用于应用程序的数据层。
在查询中使用位置参数。
如果使用的是纯JDBC,请使用PreparedStatement执行查询。通过权限和授予限制应用程序对数据库的访问。
不要将敏感的错误代码和消息返回给最终用户。
请进行适当的代码审查,以免开发人员意外地编写不安全的SQL代码。
使用SQLMap之类的工具来查找和修复应用程序中的SQL Injection漏洞。