您的位置:首页 > 数据库 > MySQL

MySQL的预编译语句的转义探究

2017-04-24 00:12 281 查看

0x00 什么是预编译语句

SQL语句,说到底也是一种类似于编程语言的东西,目的是让程序员更友好的操纵数据库,既然这样,它肯定也存在一个SQL语句的编译过程,将其转化为数据库所能执行的命令。顾名思义,预编译语句就是将需要执行SQL语句预先进行编译后缓存起来,下次使用的时候直接越过了编译这步,理论上是会比普通的SQL查询拥有更好的性能。我这里的重点不是关注它的性能,而是大家都说,预编译可以防止SQL注入,事实是怎样的呢?我们平时说的参数化查询,又是怎么一回事?

0x01 搭建环境

我们需要了解应用程序和数据库交互时,一些支持预编译的模块(如PreparedStatement)进行了怎样的处理,数据库收到的究竟是怎样的数据。我们通过两种方式来观察传递结果:开启MySQL的日志记录用wireshark抓包

首先,打开MySQL的日志记录:

这里遇到了一点小坑,寻找
my.ini
配置文件的时候,以为位置是在默认安装根目录下(C:\Program Files\MySQL\MySQL Server 5.7),结果发现并没有。查看文档,说依照以下顺序搜索配置文件:

%PROGRAMDATA%\MySQL\MySQL Server 5.7\my.ini
%PROGRAMDATA%\MySQL\MySQL Server 5.7\my.cnf
%WINDIR%\my.ini
%WINDIR%\my.cnf
C:\my.ini
C:\my.cnf
......


我在cmd里 echo %PROGRAMDATA%,结果输出的是:



接下来找到
my.ini
,在
[mysqld]
中修改以下两个参数:

...
general-log=1   # 默认是0,1代表开启
general_log_file="E:/mysql/logs/mysql_general.log"
...


接下来我们新建一个简单的Java程序,来测试一下:

try {
Class.forName(name);//指定连接类型
conn = DriverManager.getConnection(url, user, password);//获取连接
pst = conn.prepareStatement("SELECT * FROM users WHERE `name`=?");//准备执行语句
pst.setString(1,"9ian1i");
rs = pst.executeQuery();
while (rs.next()){
String name = rs.getString("name");
System.out.println(name);
}


其中,users表中的数据如下:



0x02 预编译的开启

如果按照上面的代码执行,通过wireshark抓包,你会发现,其实根本没有所谓的“预先编译”这一过程:



Java程序直接将完整的SQL语句传给了数据库,问题出在哪儿?网上有些人此时就说,MySQL不支持预编译。其实是默认不开启,你需要显式的声明,即在连接数据库的时候,加上:
useServerPrepStmts=true
,接下来我们再试一次:



预编译模式打开了!



我们同时看一下数据库的日志记录:



同样验证了我们的想法。

0x03 参数化查询

还有一种说法,说是参数化查询的时候MySQL的连接驱动进行了转义处理,所谓的防止SQL注入其实也是在代码层完成的。

记得在写SQL语句的时候,我们用占位符(?)替换了用户可控的参数,这里其实就是叫参数化查询,那当我们输入带有定界符(’)的字符串时,它会怎么处理?

我们首先直接看一下MySQL连接驱动的源代码:

public void setString(int parameterIndex, String x) throws SQLException {
synchronized(this.checkClosed().getConnectionMutex()) {
if(x == null) {
this.setNull(parameterIndex, 1);
} else {
this.checkClosed();

......
......

String parameterAsString = x;
boolean needsQuoted = true;
if(this.isLoadDataQuery || this.isEscapeNeededForString(x, stringLength)) {
needsQuoted = false;
buf = new StringBuilder((int)((double)x.length() * 1.1D));
buf.append('\'');

for(int i = 0; i < stringLength; ++i) {
char c = x.charAt(i);
switch(c) {
case '\u0000':
buf.append('\\');
buf.append('0');
break;
case '\n':

......
......
case '"':
if(this.usingAnsiMode) {
buf.append('\\');
}

buf.append('"');
break;
case '\'':
buf.append('\\');
buf.append('\'');
break;
case '\\':
buf.append('\\');
buf.append('\\');
break;
case '¥':
case '₩':
......
......


看似在做转义,但认真看代码你就会发现,竟然没有包括单引号!!!

为了验证我们的想法,依旧通过抓包和日志记录来观察,将查询参数改为
9ian'1i




再看看日志记录:



事实证明,参数化查询的转义工作是在MySQL数据库那一端完成的,而不是在代码层。

最开始我其实也认同在代码层这种看法,特别是这篇文章竟然还有好几个赞:占位符,SQL注入? ,所以说还是不能光看别人的东西,要自己动手去实践才能明白。

0x04 PHP和Python

目前主流的后端语言除了Java还有PHP和Python,我们都一并测试一下。

首先是PHP:



抓包:



执行日志:



发现PHP也是这样。

然后是Python:

Python这里有点麻烦,首先我们尝试利用%占位符:

#!/usr/bin/env python
# coding=UTF-8

import MySQLdb

con = MySQLdb.connect(host='127.0.0.1', db='test', user='root', passwd='123456')
cur = con.cursor()
cur.execute("SELECT * FROM users WHERE `name`=%s", "9i'an")
print cur.fetchone()


发现只是单纯的在代码层进行了转义。



尝试用?占位符,但是报错了。 我在Python核心编程中也没有查到预编译相关的东西。

怎么办?有个第三方库,叫做 oursql (要翻墙),网上说支持原生预编译指令,我们首先安装它:



安装方法大家应该看明白了,左边是ubuntu,右边是windows。

我们写个简单的查询:

#!/usr/bin/python
# -*- coding: utf-8 -*-
import oursql

con = oursql.connect(host='127.0.0.1', user='root', passwd='root',db='test', port=3306)
cur = con.cursor()
cur.execute('SELECT * FROM users WHERE `name`=?', ("9ia'n1i",))

print cur.fetchone()


看一下日志:



的确支持预编译。

0x05 小结

MySQL(我用的5.7)是支持预编译的,且转义是在数据库中进行的,并不是在代码层完成。

实践是检验真理的唯一标准。

主流语言都支持预编译,Python的比较麻烦,需要使用第三方模块。PHP还有个PDO,预编译其实差不多,就没有再贴。

参考文档就不贴了,太杂了,参差不齐。

推荐Python第三方库 oursql : https://pythonhosted.org/oursql/

中间的坑挺多,MySQL的常规日志在5.7中名字叫
general_log
,并不是
log
,linux下配置文件也不在
/etc/my.cnf
,而是在
/etc/mysql/my.cnf
,且里面啥都没有,需要自己添加
[mysqld]
标签。

如有错误还望大家能不吝指出。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  mysql 预编译