简单的PDO技术以及预处理方法预防SQL注入

SQL注入是十大漏洞之一,危害程度高,可能会导致数据库中的信息泄露,用户得到管理员权限等安全隐患

产生SQL注入点的原因有多种多样,但归根结底是采用了动态拼接SQL语句的方法并且没有做好相应的过滤用户数据的措施,例如

<?php
	$name = $_POST['userName'];
	$password = $_POST['userPassword'];
        if($name == null || $password == null){
		header("location:login.php");
	}
	$con=new mysqli("127.0.0.1","root","root","php10");
	if ($con->connect_error)
		die('Connect error: ' . $con->connect_error);
	$con->select_db('login');
	$sql = "SELECT * FROM login WHERE userName = '{$name}' AND userPassword = '{$password}'";
	$result=$con->query($sql);
	$num_users = $result->num_rows;
?>

在上述PHP代码中采用的就是拼接的方法,用户输入userName以及userPassword的数据,然后将userName以及userPassword拼接至$sql中,使得$sql存储一条完整的SQL查询语句,再通过mysqli中的query方法执行$sql的查询语句,num_rows方法来返回结果集中行的数目。最后,网页通过判断$num_users这个变量的值来确定数据库中是否存在用户输入的数据来决定是否允许用户登录。例如:用户输入用户名:admin,输入密码:password,$sql被拼接为:

$sql = "SELECT * FROM login WHERE userName = ' admin ' AND userPassword = ' password ' ";

结果则是存在该用户 ,所以允许用户登录

但是,如果此时用户是一名恶意用户,他输入用户名:' or 1 = 1 # ,密码输入:123(这里密码可以随意输入任何值),此时$sql被拼接为:

$sql = "SELECT * FROM login WHERE userName = ' ' or 1 = 1 # ' AND userPassword = '123' ";

可以明显看到上述SQL语句在语法上是没有任何问题的,并且放入query方法中同样能执行,代码可以正常运行并且允许用户进行登录操作,但是却不存在用户名:' or 1 = 1 # 和密码:123这样一个用户,原因如下:

$sql = "SELECT * FROM login WHERE userName = ' ' or 1 = 1 # ' AND userPassword = '123' ";

可以看到,恶意用户输入的第一个字符是一个英文的单引号 ' ,而这个单引号与之前的$sql中存储的第一个单引号 ' 发生了闭合,也就是说相当于用户在userName这一栏填入了一个空值(两个单引号之间没有字符串);紧接着恶意用户输入的是or 1 = 1,这是一组逻辑判断的语句,or就相当于C语言中的 || ,相当于逻辑或,所连接的两个逻辑判断只要有一个为真,其整体就为真值,也就是,就算这里不允许用户名填入空值,只要用or连接一个1 = 1的恒真值,整个$sql就为真,不会报错;最后,恶意用户输入了一个 # ,将之后的语句全部注释了。此时,SQL语句为真,代码继续运行导致恶意用户登录成功。

另外,恶意用户还可以将用户名和密码同时输入通用型弱口令:

' or ' ' = ' (全是单引号)

这样,$sql被拼接为:

$sql = "SELECT * FROM login WHERE userName = ' ' or ' ' = ' ' AND userPassword = ' ' or ' ' = ' ' ";

这样逻辑上就是构造了 空 = 空 这样一个恒为真值的语句,$sql也是没有语法错误的,恶意用户也可以顺利登录。

初识SQL注入介绍了防止动态拼接SQL语句造成SQL注入的方法了,就是构造一个可以过滤的函数,用此函数将用户输入的数据过滤一遍后再进行处理:

<?php
	$name = test_input($_POST['userName']);
	$password = test_input($_POST['userPassword']);
        if($name == null || $password == null){
		header("location:login.php");
	}
	$con=new mysqli("127.0.0.1","root","root","php10");
	if ($con->connect_error)
		die('Connect error: ' . $con->connect_error);
	$con->select_db('login');
	$sql = "SELECT * FROM login WHERE userName = '{$name}' AND userPassword = '{$password}'";
	$result=$con->query($sql);
	$num_users = $result->num_rows;

        function test_input($data) {
  		$data = trim($data);						//去除字符串首尾处的空白字符、制表符、换行符、回车符
  		$data = stripslashes($data);				//去除字符串中的反斜线字符,如果有两个连续的反斜线,则只去掉一个
  		$data = htmlspecialchars($data);			//把一些预定义的字符转换为 HTML 实体
  		$data = addslashes($data);					//对通过get,post和cookie传递过来的参数的单引号和双引号以及null前加“\”进行转义
  		$data = strip_tags($data);					//去除HTML、PHP中的标签
  		return $data;
	}
?>

后来了解到,这样过滤不是预防SQL注入的最好方法,最好的方法应当是使用PDO及其预处理:

PHP 数据对象 (PDO) 扩展为PHP访问数据库定义了一个轻量级的一致接口。

PDO 提供了一个数据访问抽象层,这意味着,不管使用哪种数据库,都可以用相同的函数(方法)来查询和获取数据。

PDO随PHP5.1发行,在PHP5.0的PECL扩展中也可以使用,无法运行于之前的PHP版本。

这里我使用的PHP版本是5.6.27,所以是可以运行PDO的,先完成前端代码:

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
		<title>登录界面</title>
	</head>
	<body>
		<form action = "actionPDO.php" method = "post">
			<div id = "main" class = "main">
				<h3>
					请输入用户名及密码
				</h3>
				<div>
					用户名:<input type="text" name="userName"><br>
					密码 :<input type="text" name="userPassword"><br>
				</div>
				</div>
					<input type="submit" name="submit" value="登录">
					<input type="reset" value="清空">
		</form>	

		<form action = "registerPDO.php" method = "post">
			<h3>
					注册
				</h3>
				<div>
					用户名:<input type="text" name="userName"><br>
					密码 :<input type="text" name="userPassword"><br>
				</div>
					<input type="submit" name="submit" value="注册">
					<input type="reset" value="清空">
		</form>
	</body>
</html>

界面如下:

注册页面的后端代码文件registerPDO.php(这里环境是phpstudy,php版本为5.6.27,MYSQL用的Navicat图形管理界面): 

<?php
	$name = $_POST['userName'];
	$password = $_POST['userPassword'];
	if($name == null || $password == null){
		header("location:loginPDO.php");
		return;
	}
	try {
	    //创建对象
	    $pdo = new PDO("mysql:host=localhost;dbname=php10", "root", "root");
	}catch(PDOException $e) {
	    echo "数据库连接失败:".$e->getMessage();
	    exit;
	}
	//$sql = "INSERT INTO login(userName,userPassword) VALUES (?,?)";
	$sql = "INSERT INTO loginPDO(userName,userPassword) VALUES (:userName,:userPassword)";
	$stmt = $pdo->prepare($sql);
	//$stmt->bindParam("1",$name);
	//$stmt->bindParam("2",$password);
	$stmt->bindParam(":userName",$name);
	$stmt->bindParam(":userPassword",$password);
	$result = $stmt->execute();
	if($result){
		echo "<script>alert('注册成功')</script>";
		echo "<h2>注册成功,即将跳转至登录界面...</h2>";
		header("refresh:3; url = //localhost/loginPDO.php");
	}
	else {
		echo "<h2>注册失败!</h2>";
	}

?>

 登录页面的后端代码文件actionPDO.php:

<?php
	$name = $_POST['userName'];
	$password = $_POST['userPassword'];
    if($name == null || $password == null){
		echo "<script>alert('用户名和密码不能为空!')</script>";
		echo "<h2>登录失败,3秒后自动跳转...</h2>";
		header("refresh:3; url = //localhost/loginPDO.php");
		return;
	}
	try {
	    //创建对象
	    $con = new PDO("mysql:host=localhost;dbname=php10", "root", "root");
	}catch(PDOException $e) {
	    echo "数据库连接失败:".$e->getMessage();
	    exit;
	}
	$sql = "SELECT * FROM loginPDO WHERE userName = :userName AND userPassword = :userPassword";
	//$sql = "SELECT * FROM login WHERE userName = ? AND userPassword = ?";
	$stmt = $con->prepare($sql);
	$stmt->bindParam(':userName',$name);    //bindParam方法与bindValue方法的区别在于bindParam的第二个参数可以传值用变量,而bindValue第二个参数只能传值用常量或字符串
	$stmt->bindParam(':userPassword',$password);
	//$stmt->bindParam('1',$name);
    //$stmt->bindParam('2',$password);
	$con->quote($name);          //quote方法是为普通的字符串添加引号
	$con->quote($password);
	$re=$stmt->execute();
	if($stmt->rowCount()!=0){
		echo "<script>alert('登录成功!')</script>";
		echo "<h2>欢迎您{$name}</h2>";
	}else{
		echo "<script>alert('登录失败!')</script>";
		echo "<h2>登录失败,3秒后自动跳转...</h2>";
		header("refresh:3; url = //localhost/loginPDO.php");
	}
?>

 数据库php10的表loginPDO,id为int类型主键并设置自动递增,userName以及userPassword均为varchar类型,三个属性均设置为非空(这里事先注册了一组数据):

由上述代码可以看到,这里采用连接数据库的操作为:

try {
        //创建对象
        $con = new PDO("mysql:host=localhost;dbname=php10", "root", "root");

}catch(PDOException $e) {
        echo "数据库连接失败:".$e->getMessage();
        exit;
    }

 创建一个PDO的对象来操作数据库。值得注意的是PDO的预处理prepare方法。

上传表单时是每次将查询发送给MySql服务器时,都必须解析该查询的语法,确保结构正确并能够执行,这是这个过程的必要步骤,但也确实带来了一些开销,同时还带来了安全隐患。做一次是必要的,但如果反复地执行相同的查询,批量插入多行并只改变列值会更高效,配合绑定参数以及使用占位符能有效避免SQL注入漏洞,同时,预处理语句会在服务器上缓存查询的语法和执行过程,只在服务器和客户端之间传输有变化的列值,以此来消除许多额外的开销。

这里介绍几个个PDO的方法实现上述过程:prepare(),execute(),bindParam(),quote();

1、使用预处理语句prepare()方法:

prepare()方法负责准备要执行的查询,语法格式如下:

PDOStatement PDO::prepare(string statement[,array driver_options]);

也就是用prepare先预处理代办的SQL语句

但是,使用准备语句的查询和以往使用的查询略有区别,因为对于每次执行迭代中要改变的值,必须使用占位符而不是具体的列值。查询支持两种不同的语法:命名参数和问号参数。

也就是说registerPDO.php和actionPDO.php有两种不同的方法(这两种方法接下来会简单介绍):

actionPDO.php

//第一种方法;
$sql = "SELECT * FROM loginPDO WHERE userName = :userName AND userPassword = :userPassword";

//第二种方法:
$sql = "SELECT * FROM loginPDO WHERE userName = ? AND userPassword = ?";

registerPDO.php 

//第一种方法:
$sql = "INSERT INTO loginPDO(userName,userPassword) VALUES (:userName,:userPassword)";

//第二种方法:
$sql = "INSERT INTO loginPDO(userName,userPassword) VALUES (?,?)";

2、执行准备查询execute()方法 

execute()方法负责执行准备好的查询。语法格式如下:

bool PDOStatement::execute([array input_parameters]);

该方法需要有每次迭代执行中替换输入的参数。可以通过两种方法实现:作为数组将值传递给方法,或者通过bindParam()方法把绑定到查询中相应的变量名或位置偏移。

3、绑定查询传递参数bindparam()方法

execute()方法中的input_parameters参数是可选的,虽然很方便,但是如果需要传递多个变量时,以这种方式提供数组会很快变得难以处理(当数组元素过多时,也就是当数据表中的列过多时,代码设计会变得特别难以阅读或出错)。使用bindParam()方法可以解决这个问题。

预防格式如下:

Boolean PDOStatement::bindParam(mixed parameter,mixed &variable[,int datatype[,int length[,mixed driver_options]]]);

注意:bindParam方法与bindValue方法的区别在于bindParam的第二个参数可以传值用变量,而bindValue第二个参数只能传值用常量或字符串(作者这里只使用了bindparam方法)

这里需要注意对应关系,不能混用(作者把这里弄错了改了很久):

actionPDO.php

//第一种写法
    $sql = "SELECT * FROM loginPDO WHERE userName = :userName AND userPassword = :userPassword";
    $stmt = $con->prepare($sql);
    $stmt->bindParam(':userName',$name);    
    $stmt->bindParam(':userPassword',$password)
    $re=$stmt->execute();

//第二种写法
    $sql = "SELECT * FROM loginPDO WHERE userName = ? AND userPassword = ?";
    $stmt = $con->prepare($sql);
    $stmt->bindParam('1',$name);
    $stmt->bindParam('2',$password);
    $re=$stmt->execute();

registerPDO.php

//第一种方法
    $sql = "INSERT INTO loginPDO(userName,userPassword) VALUES (:userName,:userPassword)";
    $stmt = $pdo->prepare($sql);
    $stmt->bindParam(":userName",$name);
    $stmt->bindParam(":userPassword",$password);
    $result = $stmt->execute();


//第二种方法
    $sql = "INSERT INTO login(userName,userPassword) VALUES (?,?)";
    $stmt = $pdo->prepare($sql);
    $stmt->bindParam("1",$name);
    $stmt->bindParam("2",$password);
    $result = $stmt->execute();

也就是如果SQL语句中使用的是 ? 占位符,相应的bindparam方法中的第一个参数就是表示传入第几个 ? 占位符,如"1","2";而如果SQL语句中使用的是 :userName,相应的bindparam方法中的第一个参数就是':userName'。如果混淆了,交叉使用这两种形式会导致代码出错。

 然后可以测试一下功能:

注册一组用户名为Eirc,密码为123的数据:

显示注册成功

 

查看一下数据库

 说明注册成功!

测试登录功能:

点击登录后登录成功,显示欢迎界面

 

注册和登录功能实现,接下来试一试是否还存在SQL注入的漏洞:

输入

用户名:' or 1 = 1 #

密码(随意输入):123

点击登录,显示登录失败:

尝试用户名和密码输入通用型弱口令:

' or ' ' = '

 点击登录,同样显示登录失败:

这样就有效预防了SQL注入,同样,使用PDO以及预处理的方法也可以防止SQL盲注的暴库操作,能有效保护好数据的安全。

总结:预防SQL注入和SQL盲注,拼接的过滤方法不是最优解,运用PDO和预处理方法是最为有效的防御方法

发布了18 篇原创文章 · 获赞 38 · 访问量 5092

猜你喜欢

转载自blog.csdn.net/qq_43592364/article/details/101149501