阅读本文需要7.5分钟
SQL注入是最常见的攻击之一,并且可以说是最危险的。由于Python是世界上最受欢迎的编程语言之一,因此了解如何防止Python SQL注入至关重要。
在本教程中,我们将学习:
- 什么是Python SQL注入以及如何防止注入
- 如何使用文字和标识符作为参数组合查询
- 如何安全地执行数据库中的查询
了解Python SQL注入
SQL注入攻击是一种常见的安全漏洞,传说中的xkcd网络漫画专门将其漫画化:
图片来源互联网
当使用Python将这些查询直接执行到数据库中时,很可能会犯可能损害系统的错误。在本教程中,将学习如何成功实现组成动态SQL查询的函数,而又不会使我们的系统遭受Python SQL注入的威胁。
设置数据库
首先,先建立一个新的PostgreSQL数据库并插入数据。
创建一个数据库
首先,创建一个新的PostgreSQL数据库拥有的用户postgres:
$ createdb -O postgres psycopgtest
这里使用命令行选项-O将数据库的所有者设置为用户postgres。指定了数据库的名称,即psycopgtest。
新数据库已经准备就绪!连接到并开始使用psql:
$ psql -U postgres -d psycopgtest psql (11.2, server 10.5) Type "help" for help.
现在以用户postgres的身份连接到数据库psycopgtest。该用户也是数据库所有者,因此将对数据库中的每个表都具有读权限。
创建数据表
接下来,需要创建一个表与一些用户信息,并添加数据到它:
psycopgtest=# CREATE TABLE users ( username varchar(30), admin boolean ); CREATE TABLE psycopgtest=# INSERT INTO users (username, admin) VALUES ('ran', true), ('haki', false); INSERT 0 2 psycopgtest=# SELECT * FROM users; username | admin ----------+------- ran | t haki | f (2 rows)
该表有两列:username和admin。admin列指示用户是否具有管理权限。我们的目标是试图滥用它。
设置Python虚拟环境
现在我们已经有了一个数据库,是时候设置Python环境了。
在一个新目录中创建虚拟环境:
(~/src) $ mkdir psycopgtest (~/src) $ cd psycopgtest (~/src/psycopgtest) $ python3 -m venv venv
运行此命令后,将创建一个名为venv的新目录。此目录将存储在虚拟环境中安装的所有包。
连接数据库
要连接到Python中的数据库,需要一个数据库适配器。
要连接到PostgreSQL数据库,需要安装Psycopg,这是Python中最流行的PostgreSQL适配器。
在终端中,激活虚拟环境并使用pip安装psycopg:
(~/src/psycopgtest) $ source venv/bin/activate (~/src/psycopgtest) $ python -m pip install psycopg2>=2.8.0 Collecting psycopg2 Using cached https://.... psycopg2-2.8.2.tar.gz Installing collected packages: psycopg2 Running setup.py install for psycopg2 ... done Successfully installed psycopg2-2.8.2
现在可以连接到数据库的了。
import psycopg2 connection = psycopg2.connect( host="localhost", database="psycopgtest", user="postgres", password=None, ) connection.set_session(autocommit=True)
使用psycopg2.connect()来创建连接。这个函数接受以下参数:
host
:数据库所在服务器的IP地址或DNS。在本例中,主机是localhost。
database:要连接的数据库的名称。
user:具有数据库权限的用户。
password:用户的密码。在大多数开发环境中
在设置连接之后,将会话配置为autocommit=True。激活自动提交意味着我们不必通过发出提交或手动管理事务。
执行查询
现在我们已经连接到数据库,准备执行一个查询:
>>> with connection.cursor() as cursor: ... cursor.execute('SELECT COUNT(*) FROM users') ... result = cursor.fetchone() ... print(result) (2,)
在SQL中使用查询参数
在前面,我们创建了一个数据库,连接到了它,并执行了一个查询。
首先,我们将实现一个函数来检查用户是否为管理员。is_admin()接受用户名并返回该用户的管理状态:
# BAD EXAMPLE. DON'T DO THIS! def is_admin(username: str) -> bool: with connection.cursor() as cursor: cursor.execute(""" SELECT admin FROM users WHERE username = '%s' """ % username) result = cursor.fetchone() admin, = result return admin
执行这个函数查询来获取给定用户名的admin列的值。使用fetchone()返回一个带有单个结果的元组。然后,将这个元组解压缩到变量admin中。
>>> is_admin('haki') False >>> is_admin('ran') True
到目前为止一切正常。但是那些不存在的用户呢?看看这段Python代码:
>>> is_admin('foo') Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 12, in is_admin TypeError: cannot unpack non-iterable NoneType object
如果当用户不存在时,将引发一个错误。这是因为.fetchone()在没有找到结果时返回None,而解包None会引发一个类型错误。
为了处理不存在的用户,在结果为None时创建一个特殊的情况:
def is_admin(username: str) -> bool: with connection.cursor() as cursor: cursor.execute(""" SELECT admin FROM users WHERE username = '%s' """ % username) result = cursor.fetchone() if result is None: # User does not exist return False admin, = result return admin
这里,我们添加了一个处理None的特殊情况。如果用户名不存在,那么函数应该返回False。如下:
>>> is_admin('haki') False >>> is_admin('ran') True >>> is_admin('foo') False
使用Python SQL注入利用查询参数
在前面的示例中,使用字符串插值表达式生成查询。然后,执行查询并将结果字符串直接发送到数据库。然而,在这个过程中我们可能忽略了一些东西。
之前我们传递给is_admin()的用户名参数。这个变量到底代表什么呢?大家可能认为username只是表示实际用户名的字符串。但是,入侵者可以很容易地利用这种疏忽,并通过执行Python SQL注入造成重大危害。
尝试检查以下用户是否是管理员:
>>> is_admin("'; select true; --") True
天呐!!!发生什么事了?
让我们再看一下实现。打印出在数据库中执行的实际查询:
>>> print("select admin from users where username = '%s'" % "'; select true; --") select admin from users where username = ''; select true; --'
结果文本包含三个语句。为了准确地理解Python SQL注入是如何工作的,我们需要分别检查每个部分。第一:
select admin from users where username = '';
这是我们想要的查询。分号终止查询,因此此查询的结果不怎么重要。第二:
select true;
这是入侵者编造的。它的设计总是返回True。
最后,将看到这一小段代码:
--'
这个代码段将消除后面的任何内容。入侵者添加了注释符号(——)来将可能放置在最后一个占位符之后的所有内容转换成注释。
当使用这个参数执行函数时,它总是返回True。例如,如果大家在登录页面中使用此函数,则入侵者可以使用用户名'登录;选择正确的;,他们将被允许进入。
更可怕的是了解表结构的入侵者可以使用Python SQL注入来造成永久性损害。例如,入侵者可以注入一条更新语句来改变数据库中的信息:
>>> is_admin('haki') False >>> is_admin("'; update users set admin = 'true' where username = 'haki'; select true; --") True >>> is_admin('haki') True
让我们再来分解一下:
';
这段代码终止了查询,就像前面的注入一样。下一次注入如下:
update users set admin = 'true' where username = 'haki';
这次将用户haki的admin更新为true 。代码如下:
select true; --
与前面的示例一样,返回true并注释掉后面所有的内容。
如果入侵者设法执行这个输入的功能,那么用户haki将成为一个管理员:
psycopgtest=# select * from users; username | admin ----------+------- ran | t haki | t (2 rows)
他们可以用用户名haki登录。(如果入侵者真的想造成伤害,那么他们甚至可以发出DROP DATABASE命令。)
提前把haki恢复到原来的状态:
psycopgtest=# update users set admin = false where username = 'haki'; UPDATE 1
为什么会这样呢?我们对用户名参数了解多少?我们只知道它应该是一个表示用户名的字符串,但是我们实际上并没有检查或执行这个断言。这可能很危险!攻击者试图利用这些东西入侵我们的系统。
待续。。。
推荐阅读
●做个Python反转字符串的实验
岁月有你 惜惜相处