本文演示简单的SQL注入攻击

何谓SQL注入?

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()


ID	Name
1	Paul
2	Tom
3	Tracy
4	Lily

上述程序中我们建立了一个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()


即将执行的SQL代码为: INSERT INTO students (name) VALUES ('Robert');DROP TABLE students;--')
---------------------------------------------------------------------------
OperationalError                          Traceback (most recent call last)
<ipython-input-2-aa5b7941ebe3> in <module>()
      8 
      9 # 检视已有的学生信息
---> 10 cursor = conn.execute("SELECT id, name from students")
     11 print('ID\tName')
     12 for row in cursor:

OperationalError: no such table: students

你将会发现,运行后,程序没有输出任何数据内容,而是返回一条错误信息:表单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关键字,那么,只要检查数据项中是否存在SQL关键字不就可以了么?的确是这样,很多数据库管理系统都是采取了这种看似『方便快捷』的过滤手法,但是这并不是一种根本上的解决办法,如果有个美国人真的就叫做『Drop Table』呢?你总不能逼人家改名字吧。

  1. 原则1:永远不要相信用户提供的数据。
  2. 原则2:转义字符

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个字符之间')


用户名必须符合字母数字字符加下划线和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()


b'Um9iZXJ0Jyk7RFJPUCBUQUJMRSBzdHVkZW50czstLQ=='
即将执行的SQL代码为: INSERT INTO students (name) VALUES ('Um9iZXJ0Jyk7RFJPUCBUQUJMRSBzdHVkZW50czstLQ==')
ID	Name
1	Paul
2	Tom
3	Tracy
4	Lily
5	Um9iZXJ0Jyk7RFJPUCBUQUJMRSBzdHVkZW50czstLQ==
b"Robert');DROP TABLE students;--"