什么是SQL注入攻击?如何防范?

一个Web应用程序,除非其特别简单,一般情况下,都是需要用到数据库的。而数据库有数据库自身的特点和安全问题。其中最为有名、最为广泛的攻击是SQL注入。而除了SQL注入,不同的数据库也还有其他安全问题需要解决。

数据库大都是支持SQL语言的(如果你想学习SQL查询语言,请访问我们的SQL语言教程),这就意味着,SQL语言本身是有预留关键字的,比如常用的select、update、delete等。但是程序语言往往没有把这些列为保留字。那么,问题就来了,如果一个别有用心的人,故意输入一个SQL查询语句,而我们没有执行相关验证,会发生什么?轻者程序错误,代码泄露,重者直接暴库,甚至危及服务器整体安全。这就是SQL注入攻击。

可见,SQL注入本质上是一种用户输入式攻击,是程序没有对用户输入进行充分验证留下的漏洞。

 

攻击示例

这里,我们假设有一个页面,接收html传输来的用户名和密码,其控件名分别为txtUserID和txtPassword,这些用户输入的用户名和密码被加到程序语句的SQL查询语句中,用来被验证用户身份。老式的ASP代码如下所示:

 

set rs=server.createobject("adodb.recordset")sql="select count(UserID) from users where UserID='"&request.form("txtUserID")&"' and Password='"&request.form("txtPassword")&"'"rs.open sql,conn,1,1

 

VB.net代码如下:

 

Dim query AsString="select count(UserID) from users where UserID='"&txtUserID.Text&"' and Password='"&txtPassword.Text&"'"

 

C#的代码如下:

 

string query ="select count(UserID) from users where UserID='"&txtUserID.Text&"' and Password='"&txtPassword.Text&"'"

 

php代码如下:

 

$query = mysql_query("select count(UserID) from users where UserID='".$_POST["txtUserID"]."' and Password='".$_POST["txtPassword.Text"]."'");

 

jsp代码如下:

 

String query="SELECT count(UserID) FROM users where UserID='"+request.getParameter("txtUserID")+"' and Password='"+request.getParameter("txtPassword")+"'";

 

我们的示例代码是很危险的!因为它们都没有经过审核验证用户的输入就将其加入到SQL查询语句中。当然,如果此时执行的验证完全只考虑XSS等其他类型的用户输入型攻击,危险仍然是存在的。让我们试图想象一下,攻击者输入了一个用户名:“'or 1=1 --”(不含双引号),那么整条SQL查询语句就会变成下面的样子:

 

SELECT * FROM users whereUserID=''or1=1--andPassword=''

 

也就是说,单引号'先是结束掉了UserID的查询,然后跟一个or语句,而1=1永远都会是true,所以错误便会出现,数据库里users 表所有的记录都将被加入程序计数,如果这时以是否取到了记录作为用户名密码是否有效的标准,那么攻击者就将会被授权。

还有更暴力的,直接会把你的表乃至整个数据库整个删除,后果极其严重:

 

SELECT * FROM users whereUserID=''or1=1; DROP DATABASE (dbname)--andPassword=''

 

 

如何防范SQL注入攻击

SQL注入能够成功的根本在于,程序没有对用户输入进行有效的验证。那么解决思路就很简单了,要么彻底验证用户的输入再将其并入SQL语句;要么将用户输入作为参数传入SQL语句,而不是简单的字符串拼接。

我们先来说第一种,将用户的输入进行彻底验证再将其并入SQL语句,是一个非常高效、简便的方法,当然,它不是绝对安全的,因为如果你验证的不够彻底,就会给攻击者留下可乘之机。不过因为其简便性,也是很多开发者选择的防范方法。那么,需要怎么验证呢?我们可以这样验证:验证输入是否含单引号(')、双引号(")、连接符(-)、分号(;)、括号(())等特殊字符(都是英文状态下的),如果用户的输入中,含有这些字符,那么一律验证不通过。这里列出一个特殊字符列表,供大家参考:

 

;|%|\|>|<|--|^|(|)|+|$|'|*|\"

 

你也可以使用正则表达式来验证,如只接受汉字、数字和英文字符:

 

[A-Za-z0-9_-\u4e00-\u9fa5]+

 

然后,我们可以考虑,将常见的SQL操作关键字,如select、update、insert、and、or、drop、alter等等一律干掉,当然,这个适用于用户不太可能会输入这些字符的情况下,具体的关键词可以参考我们的SQL语言教程。

我们再来看第二种,将用户输入作为参数传给处理程序,这种方法相较第一种方法更加安全,因为你如果忘记过滤某个特殊字符也可能带来危险,但这种方法稍显麻烦。PHP、.net、jsp都提供参数化查询的方式,它们都把用户输入作为单纯的字符串处理。

1、ASP.net

.net中,SQL存储过程可以接受多种类型的参数,参数化SQL查询使用 @作为前缀,C#代码如下例所示:

 

string sql="select count(email) from users where email= @Username and password = @Password";//使用其他地方定义的连接字符串SqlConnection connection =newSqlConnection(connectionString);//这里txtName 和 txtPass 是 runat="server" 的Web控件SqlCommand command =newSqlCommand(sql,connection);command.Parameters.Add("@Username",txtName.Text);command.Parameters.Add("@Password",txtPass.Text);connection.Open();int count=(int)command.ExecuteScalar();connection.Close();

 

VB.net代码如下所示:

 

Dim sql AsString="select count(EmailName) from users where email = @Username and password = @Password"'使用其他地方定义的连接字符串Dim connection As New SqlConnection(connectionString)Dim command As New SqlCommand(sql,connection)'这里txtName 和 txtPass 是 runat="server"的Web控件command.Parameter.Add("@Username",txtUser.Text)command.Parameter.Add("@Password",txtPass.Text)connection.Open()Dim count AsIntegercount=Ctype(command.ExecuteScalar(),Integer)connection.Close()

 

2、PHP

PHP通过使用预编译语句(prepared statements)和参数化查询(parameterized queries)。这些sql语句从参数,分开的发送到数据库服务端,进行解析。

有两种方式去完成这个,一是使用PDO对象(适用任何数据库驱动);第二是使用MySqli。使用PDO对象的代码如下:

 

$stmt = $pdo->prepare('SELECT * FROM employees WHERE name = :name');$stmt->execute(array('name'=> $name));foreach($stmt as $row){// do something with $row}

 

当使用PDO去连接Mysql数据库时,真正的预处理默认并没有开启。这时就应该关闭模拟的预处理语句:

 

$dbConnection =new PDO('mysql:dbname=dbtest;host=127.0.0.1;charset=utf8','user','pass');$dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES,false);$dbConnection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

 

第1行的setAttribute()函数是必须的,作用是关闭模拟预处理,并使用真正的预处理语句。这将保证语句和值在被交到Mysql服务器上没有被解析。而第2行的setAttribute()函数不是必要的,但是推荐你加上去。这样,脚本在遇到致命错误(Fatal Error)时不会停止运行,并且我们可以去捕获PDOException异常。此外,你可以在构造函数里设置字符集(charset),但旧版本的PHP(<5.3.6)会忽略在DSN中设置的字符集参数。

第二种,使用MySqli的代码如下:

 

$stmt = $dbConnection->prepare('SELECT * FROM employees WHERE name = ?');$stmt->bind_param('s', $name);$stmt->execute();$result = $stmt->get_result();while($row = $result->fetch_assoc()){// do something with $row}

 

3、JSP

JSP中使用PreparedStatement类中的诸多setXxxx方法来实现。PreparedStatement接口继承Statement,并与之在两方面有所不同:一、PreparedStatement 实例包含已编译的 SQL 语句。这就是使语句“准备好”。包含于 PreparedStatement 对象中的 SQL 语句可具有一个或多个 IN 参数。IN参数的值在 SQL 语句创建时未被指定。相反的,该语句为每个 IN 参数保留一个问号(“?”)作为占位符。每个问号的值必须在该语句执行之前,通过适当的setXXX 方法来提供。二、由于 PreparedStatement 对象已预编译过,所以其执行速度要快于 Statement 对象。因此,多次执行的 SQL 语句经常创建为 PreparedStatement 对象,以提高效率。

作为 Statement 的子类,PreparedStatement 继承了 Statement 的所有功能。另外它还添加了一整套方法,用于设置发送给数据库以取代 IN 参数占位符的值。同时,三种方法 execute、 executeQuery 和 executeUpdate 已被更改以使之不再需要参数。这些方法的 Statement 形式(接受 SQL 语句参数的形式)不应该用于 PreparedStatement 对象。

以下的代码段(其中 con 是 Connection 对象)创建包含带两个 IN 参数占位符的 SQL 语句的 PreparedStatement 对象:

 

PreparedStatement pstmt = con.prepareStatement("UPDATE table4 SET m = ? WHERE x = ?");

 

pstmt 对象包含语句 "UPDATE table4 SET m = ? WHERE x = ?",它已发送给DBMS,并为执行作好了准备。

在执行 PreparedStatement 对象之前,必须设置每个 ? 参数的值。这可通过调用 setXXX 方法来完成,其中 XXX 是与该参数相应的类型。例如,如果参数具有Java 类型 long,则使用的方法就是 setLong。setXXX 方法的第一个参数是要设置的参数的序数位置,第二个参数是设置给该参数的值。例如,以下代码将第一个参数设为 123456789,第二个参数设为 100000000:

 

pstmt.setLong(1,123456789);pstmt.setLong(2,100000000);

 

一旦设置了给定语句的参数值,就可用它多次执行该语句,直到调用clearParameters 方法清除它为止。在连接的缺省模式下(启用自动提交),当语句完成时将自动提交或还原该语句。

如果基本数据库和驱动程序在语句提交之后仍保持这些语句的打开状态,则同一个 PreparedStatement 可执行多次。如果这一点不成立,那么试图通过使用PreparedStatement 对象代替 Statement 对象来提高性能是没有意义的。

利用 pstmt(前面创建的 PreparedStatement 对象),以下代码例示了如何设置两个参数占位符的值并执行 pstmt 10 次。如上所述,为做到这一点,数据库不能关闭 pstmt。在该示例中,第一个参数被设置为 "Hi"并保持为常数。在 for 循环中,每次都将第二个参数设置为不同的值:从 0 开始,到 9 结束。

 

pstmt.setString(1,"Hi");for(int i =0; i <10; i++){ pstmt.setInt(2, i); int rowCount = pstmt.executeUpdate();}

 

setXXX 方法中的 XXX 是 Java 类型。它是一种隐含的 JDBC 类型(一般 SQL 类型),因为驱动程序将把 Java 类型映射为相应的 JDBC 类型(遵循该 JDBCGuide中§8.6.2 “映射 Java 和 JDBC 类型”表中所指定的映射),并将该 JDBC 类型发送给数据库。例如,以下代码段将 PreparedStatement 对象 pstmt 的第二个参数设置为 44,Java 类型为 short:

 

pstmt.setShort(2,44);

 

驱动程序将 44 作为 JDBC SMALLINT 发送给数据库,它是 Java short 类型的标准映射。

程序员的责任是确保将每个 IN 参数的 Java 类型映射为与数据库所需的 JDBC 数据类型兼容的 JDBC 类型。不妨考虑数据库需要 JDBC SMALLINT 的情况。如果使用方法 setByte ,则驱动程序将 JDBC TINYINT 发送给数据库。这是可行的,因为许多数据库可从一种相关的类型转换为另一种类型,并且通常 TINYINT 可用于SMALLINT 适用的任何地方。

预处理语句对象PreparedStatement,使用PreparedStatement进行添加数据,更新数据,删除数据和查询数据。

添加数据的例子(注意一定要注意大小写):

 

<%@page language="java" contentType="text/html;charset=gb2312"%>

<%@pageimport="java.sql.*" %>

获得第二条记录开始的三条记录

<%

String url ="jdbc:mysql://localhost:3306/javaweb";//连接数据库的url地址

String user ="root";//登录数据库的用户名

String password ="zhangda890126;;";//登录数据库的用户名的密码

Connection conn =null;//链接对象

PreparedStatement pstmt =null;//语句对象

ResultSet rs = null;//结果集对象

try{Class.forName("com.mysql.jdbc.Driver");//加载JDBC驱动程序

conn =DriverManager.getConnection(url,user,password); //连接数据库

}catch(ClassNotFoundException e){out.println("找不到驱动类");//抛出异常时,提示信息

}catch(SQLException e){out.println("链接MySQL数据库失败");//处理SQLException异常

}try{String adduser ="INSERT INTO user (userid,username,password) VALUES(null,?,?)";//添加一条用户信息

pstmt = conn.<span style="color:#e53333;"><b>prepareStatement</b></span>(adduser);//创建预处理语句对象PreparedStatement//设置参数pstmt.setString(1,"YAO");pstmt.setString(2,"yao");//执行语句pstmt.executeUpdate();

}catch(SQLException e){out.println("添加用户信息失败");

}try{if(pstmt !=null){pstmt.close();conn =null;

}if(conn !=null){conn.close();conn =null;}}catch(Exception e){out.println("数据库关闭失败");}

%>

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值