SQL注入是一种非常常见的数据库攻击手段,SQL注入漏洞也是网络世界中最普遍的漏洞之一。
SQL数据库的操作是通过SQL语句来执行的,而无论是执行代码还是数据项都必须写在SQL语句之中,这就导致如果我们在数据项中加入了某些SQL语句关键字(比如说SELECT、DROP等等),这些关键字就很可能在数据库写入或读取数据时得到执行。
下面我们先使用SQLite建立一个学生档案表。
In [3]:
import sqlite3
# 连接数据库
conn = sqlite3.connect('SQL-Injection-Attack.db')
# 建立新的数据表
conn.executescript('''DROP TABLE IF EXISTS students;
CREATE TABLE students
(id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT(99) NOT NULL);''')
# 插入学生信息
students = ['Paul','Tom','Tracy','Lily']
for name in students:
query = "INSERT INTO students (name) VALUES ('%s')" % (name)
conn.executescript(query);
# 检视已有的学生信息
cursor = conn.execute("SELECT id, name from students")
print('ID\tName')
for row in cursor:
print('{0}\t{1}'.format(row[0], row[1]))
conn.close()
上述程序中我们建立了一个test.db数据库以及一个students数据表,并向表中写入了四条学生信息。如果后续不慎删除了表格,也可以重新执行这段代码。
那么SQL注入又是怎么一回事呢?我们尝试再插入一条恶意数据,数据内容就是"Robert');DROP TABLE students;--",看看会发生什么情况。
In [2]:
conn = sqlite3.connect('SQL-Injection-Attack.db')
# 插入包含注入代码的信息
name = "Robert');DROP TABLE students;--"
query = "INSERT INTO students (name) VALUES ('%s')" % (name)
print('即将执行的SQL代码为:',query)
conn.executescript(query)
# 检视已有的学生信息
cursor = conn.execute("SELECT id, name from students")
print('ID\tName')
for row in cursor:
print('{0}\t{1}'.format(row[0], row[1]))
conn.close()
你将会发现,运行后,程序没有输出任何数据内容,而是返回一条错误信息:表单students无法找到!(OperationalError: no such table: students)
这是为什么呢?问题就在于我们所插入的数据项中包含SQL关键字DROP TABLE,这两个关键字的意义是从数据库中清除一个表单。而关键字之前的Robert');使得SQL执行器认为上一命令已经结束,从而使得危险指令DROP TABLE得到执行。也就是说,这段包含DROP TABLE关键字的数据项使得原有的简单的插入姓名信息的SQL语句
"INSERT INTO students (name) VALUES ('Robert')"
变为了同时包含另外一条清除表单命令的语句
"INSERT INTO students (name) VALUES ('Robert');DROP TABLE students;--"
而SQL数据库执行上述操作后,students表单被清除,因而表单无法找到,所有数据项丢失。
那么,如何防止SQL注入问题呢?
大家也许都想到了,注入问题都是因为执行了数据项中的SQL关键字,那么,只要检查数据项中是否存在SQL关键字不就可以了么?的确是这样,很多数据库管理系统都是采取了这种看似『方便快捷』的过滤手法,但是这并不是一种根本上的解决办法,如果有个美国人真的就叫做『Drop Table』呢?你总不能逼人家改名字吧。
In [4]:
# 原则1:永远不要相信用户提供的数据。在下面的例子中,用户名被限制为字母数字字符加下划线和8到20个字符之间的长度
conn = sqlite3.connect('SQL-Injection-Attack.db')
import re
# 将正则表达式编译成Pattern对象
pattern = re.compile(r'^\w{8,20}$')
name = "Robert');DROP TABLE students;--"
if pattern.match('name'):
print('即将执行的SQL代码为:',query)
conn.executescript(query)
else:
print('用户名必须符合字母数字字符加下划线和8到20个字符之间')
In [5]:
# 原则2:转义字符。在下面的例子中,用户名被base64编码,以确保不会出现奇奇怪怪的东西
conn = sqlite3.connect('SQL-Injection-Attack.db')
import base64
name = "Robert');DROP TABLE students;--".encode()
name = base64.b64encode(name)
print(name)
query = "INSERT INTO students (name) VALUES ('%s')" % (name.decode('utf-8'))
print('即将执行的SQL代码为:',query)
conn.executescript(query)
# 检视已有的学生信息
cursor = conn.execute("SELECT id, name from students")
print('ID\tName')
for row in cursor:
print('{0}\t{1}'.format(row[0], row[1]))
# 在此情况下,输出需要base64进行解码,否则输出为乱码
print(base64.b64decode(row[1]))
conn.close()