Python - MySqldb 防sql注入 - 底层原理分析

引言

上周,我写的项目(基于tornado框架),被同事质疑存在sql注入风险。虽然我的代码确实写的像一坨屎,但是有人当众指出这是一坨屎,让我很难下台。为了证明我的代码虽然是一坨屎,但是它是一坨安全的,至少在防sql注入方面是安全的屎,我决定研究一下python中对mysql的操作(基于MySQLdb库)是怎么做到防sql注入的,是否真的可以防sql注入,以给质疑我的同事一个答复,也为我的代码正名。

Google: python mysqldb injection ,或者百度: python mysql 防注入,很容易得到一个结论:
利用MySQLdb的cursor的execute方法,将众参数作为一个tuple传入execute,避免通过拼接字符串的方式直接传入一个完整的sql语句,就可以做到防sql注入,大致情形如下:

# 可以防止sql注入的写法
cur.execute('select * from table where col=%s and row=%s',(user_col_in, user_row_in))
 
# 不能防止sql注入的写法(拼接字符串)
cur.execute('select * from table where col=%s and row=%s' % (user_keyin, user_row_in))

网上的文章/回答都告诉你应该这么做,但是没有人解释底层的原理,即MySQLdb的execute方法做了什么处理,才实现了防注入的效果。显然,如果只是应用相关接口,不需要知道底层原理也足够了。但是我是为了回应他人的质疑,就只能选择深挖,不然别人一句“网上说这样防注入,就真的防注入?”就可以把我怼得哑口无言。其实大致也可以猜到这个底层实现应该就是对一些特殊字符进行转义,但是我得"Show the Code",而不是在这猜想,否则难以令人信服。本着对我的屎负责的原则,本人决定进一步深挖execute()方法底层原理。

源码剖析

整个开源项目链接 : MySQLdb1
打开MySQLdb文件夹下的cursors.py文件,可以找到execute方法的源码:

在这里插入图片描述
这里只需注意我红框圈出来的代码,下面的try…except部分就是执行该query语句和异常处理,与我们今天的防注入主题无关。
这里的 ‘%’ 就是python或者c的printf()中的那个格式化符号,用于将后面的参数填入query,可以看到,这边对item做了一层’literal’处理,继续跟踪literal函数:
在这里插入图片描述
发现该函数是调用escape()方法。其中self.encoders,顾名思义,应该进行一些“编码”转换的handler,跟进该参数,最终形式大概是:
在这里插入图片描述
其实就是一些转换函数,用于将各种类型类型都转化为str:
在这里插入图片描述
看完了self.encoders,回到这个escape方法。这个方法你在MySQLdb中是找不到的,因为这个方法是从_mysql中import的,而_mysql是用c写的,没办法,继续跟进_mysql.c:
(关于Python如何调用C函数,见附录:附:浅析Python Programs 调用C函数

可以看到Python的escape()方法在这被转化为了(PyCFunction)_mysql_escape,跟入该函数:
在这里插入图片描述
其中PyArg_ParseTuplePyMapping_Check都是对参数进行一些校验,具体功能与用法在官方文档中都有解释,这里不再赘述。
只需注意要被escape的对象(obeject),被存在了o对应的地址中,python中escape的第二个参数(self.encoders)被存在了d对应的地址中。
这里我们又发现了下一步该去研究哪个函数:_escape_item:
在这里插入图片描述
可以看到这个函数主要就是做了如下几件事:

  1. 得到传入参数在python中的类型:PyObject_Type(item)相当于Python中的type(item)
  2. 拿着这个类型,去参数"d",即最初的self.encoders中寻找相应的转换函数:PyObject_GetItem(d, itemtype)相当于python中的d.get(itemtype)
  3. 如果没有找到相应的转换函数,就将转换函数默认设置为Unicode(Python3)或者String(Python2)的转换函数;
  4. 通过PyObject_CallFunction()再调用Python中相应的转换函数。

什么?绕了一圈又绕回Python了?不要慌,让我们来看看Python中的转换函数在干嘛,又回到一切开始的地方:

在这里插入图片描述

可以看到,对于比较简单的,不具有sql注入可能的类型(bool、int(IntType的转换函数也是Thing2Str)、long),这里的处理是直接经过str()函数处理就返回。而对于String和Set等这种比较复杂的类型,需要再经过string_literal()的处理,其实你可以往上翻翻,找到那张conversions的图,可以发现default就是Thing2Literal,即string_literal()

而这个string_literal()又是用c语言写的,又跳回去了。。。详情如下:
在这里插入图片描述
前面都是一些Python与C之间的一些类型转换,将Python中的String转化为C中的String,具体原理这里不再赘述,网上都有。

这里主要看红框中的代码:
发现两个函数mysql_real_escape_string()mysql_escape_string()
看这函数名字,不用说了,这一定就是最底层那个对参数进行反注入处理的函数了。我的天呐,终于给我找到了,藏的这么深。
下面来分析一下这两个函数:

这里再往上翻一点,可以看到

#if MYSQL_VERSION_ID < 32321
	len = mysql_escape_string(out+1, in, size);

大致意思就是MySQL低于某个版本时,使用mysql_escape_string()

然后可以看到

	if (self && self->open)
		len = mysql_real_escape_string(&(self->connection), out+1, in, size);

这个意思也很明显,当建立了数据库连接时,使用mysql_real_escape_string()
为什么mysql_real_escape_string()只能在存在数据库连接时使用呢?之所以需要它是因为,转义功能取决于服务器使用的字符集,这样mysql_real_escape_string()就可以根据服务器使用的字符集进行更加精确的转义了。

读到这大家大概也懂了,mysql_real_escape_string()相比于mysql_escape_string()肯定是更高级的,除非在没办法的情况下,否则优先考虑使用mysql_real_escape_string()

这两个函数在_mysql.c中并没有定义,那显然是某个头文件中的函数了,往上翻一翻,果不其然:

#include "mysql.h"

下面是关于unsigned long mysql_real_escape_string(MYSQL *mysql, char *to, const char *from, unsigned long length)的描述:

该函数用于创建可在SQL语句中使用的合法SQL字符串。

按照连接的当前字符集,将“from”中的字符串编码为转义SQL字符串。将结果置于“to”中,并添加1个终结用NULL字节。编码的字符为NUL
(ASCII 0)、‘\n’、‘\r’、‘\’、‘’’、‘"’、以及Control-Z(请参见9.1节,“文字值”)。(严格地讲,MySQL仅需要反斜杠和引号字符,用于引用转义查询中的字符串。该函数能引用其他字符,从而使得它们在日志文件中具有更好的可读性)。

“from”指向的字符串必须是长度字节“long”。必须为“to”缓冲区分配至少length*2+1字节。在最坏的情况下,每个字符或许需要使用2个字节进行编码,而且还需要终结Null字节。当mysql_real_escape_string()返回时,“to”的内容是由Null终结的字符串。返回值是编码字符串的长度,不包括终结用Null字符。

如果需要更改连接的字符集,应使用mysql_set_character_set()函数,而不是执行SET NAMES (或SET CHARACTER SET)语句。mysql_set_character_set()的工作方式类似于SET NAMES,但它还能影响mysql_real_escape_string()所使用的字符集,而SET NAMES则不能。

至于mysql_escape_string(),现在官方文档中已经不再推荐使用该函数了:
在这里插入图片描述
mysql_real_escape_string()内部具体如何实现的,我打开mysql.h,只找到了函数的声明,具体的定义没有找到。
最后在网上找到了libmysql/libmysql.cc,终于见到了mysql_real_escape_string()的庐山真面目:
在这里插入图片描述
可见底层调用的是mysql_real_escape_string_quote(),注意最后一个参数,传入的是'\''
这段代码的逻辑就是,当没有开放的数据库连接,即取不到数据库对应的字符集时,调用mysql_real_escape_string_quote()方法。
该方法位于mysys/charset.cc,打开此文件,我们跟入escape_string_for_mysql()
在这里插入图片描述
在这里插入图片描述
其实注释中已经说的很清楚了,就是在特殊字符前加上backslashes(下划线)。整个逻辑就是通过switch…case来实现的,代码这么长主要是加入了很多参数判断和异常处理。
至此,最底层的逻辑终于呈现在眼前。从MySQLdb的execute()到最终的这个escape_string_for_mysql(),经过了十几个函数的调用,其实最后的逻辑就是字符串的替换,也验证了最初的猜想,只是想找到最底层的调用确实要费点精力。。。

总结

Python的MySQLdb经过层层封装,各种调用,最后调用了mysql.hmysql_real_escape_string()mysql_escape_string()这两个C API接口实现了对特殊字符的转义,从而起到了防止sql注入的作用。这个mysql.h头文件是在安装mysql时自带的,mysql官方出品,对安全性有保证,同时也不用担心转义的字符不会被mysql识别从而引发一些问题。

同时经过这次源码剖析,也得出一个结论。无论使用什么语言对mysql进行操作,最终一定要调用mysql官方的mysql_real_escape_string()接口(这个函数不光有C的API,市面上各大主流语言API该函数都有),这样就可以应用官方提供的防注入处理函数来防止sql注入的发生。

更重要的是,一定要注意使用execute(query_str, (query_tuple))的方式对mysql进行操作,而不是execute("自行拼接的sql语句"),否则无论第三方库封装地再好,都救不了你。

附:浅析Python Programs 调用C函数

其实我原先也不知道python是如何调用c的函数的,正好借此机会研究了一下。
具体实现在python官方文档中:Python2 都讲的很清楚了,我这里再简单翻译,用中文总结一下。

  1. #include "Python.h"
  2. 在C代码中创建一个“Method table”,在其中罗列函数以实现Python的函数在C源代码中的注册/绑定:
static PyMethodDef YourMethods[] = {
…
{“PythoFunctionName”, CFunctionName, METH_VARARGS,
“docstring for the function.”},
…
{NULL, NULL, 0, NULL} /* Sentinel */
};

其中:
第一个Entry “PythoFunctionName”,指Python中的函数名;
第二个Entry CFunctionName指C中的函数名;
第三个Entry METH_VARARGS是一个标志位,用来表明传入参数的类型,具体详见官方文档;
第四个Entry 指的是python中的docstring,即解释文档,可以写也可以不写。

例如,在_mysql.c中escape()方法的注册如下:
在这里插入图片描述
在这里插入图片描述

  1. 构建(PyCFunction)CFunctionName
  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值