每天学习一点Python——SQL注入攻击与防御
大家好,今天我们要聊一个非常实用但又容易被忽视的话题——SQL注入攻击。
相信很多初学者在写数据库操作代码时,都遇到过这样的情况:代码明明看起来没问题,但一执行就报错,而且错误信息特别奇怪。今天,我们就通过一个完整的例子,彻底搞明白这是怎么回事。
一、先来创建一个简单的数据库
让我们从最基础的开始。首先,我们需要创建一个数据库和一张表:
import sqlite3
sql = """
DROP TABLE IF EXISTS People;
CREATE TABLE People(
FirstName TEXT,
LastName TEXT,
Age INT
);
INSERT INTO People VALUES(
'Ron',
'Obvious',
42
);
"""
with sqlite3.connect("test_database.db") as connection:
cursor = connection.cursor()
cursor.executescript(sql)
代码逐行解释:
| |
|---|
import sqlite3 | |
sql = """...""" | |
DROP TABLE IF EXISTS People; | |
CREATE TABLE People(...); | 创建新表,包含三个字段:FirstName(文本)、LastName(文本)、Age(整数) |
INSERT INTO People VALUES(...); | |
with sqlite3.connect("test_database.db") as connection: | |
cursor = connection.cursor() | |
cursor.executescript(sql) | 执行多条SQL语句(注意是executescript,不是execute) |
执行这段代码后,我们就有了一个包含一条记录的数据库。
二、批量插入数据
接下来,我们用更高效的方式插入多条数据:
people_values = (("Ron", "Obvious", 42), ("Luigi", "Vercotti", 43), ("Arthur", "Belling", 28))
with sqlite3.connect("test_database.db") as connection:
cursor = connection.cursor()
cursor.executemany("INSERT INTO People VALUES(?, ?, ?)", people_values)
重点理解:三个问号是什么意思?
这里出现了三个?,很多初学者会困惑:为什么要用问号?直接写值不行吗?
这三个问号是参数占位符,它们对应的是:
为什么正好是三个?看看我们的数据:
- •
("Ron", "Obvious", 42) → 三个值 - •
("Luigi", "Vercotti", 43) → 三个值 - •
("Arthur", "Belling", 28) → 三个值
executemany的工作原理:
- 1. 准备SQL模板:
INSERT INTO People VALUES(?, ?, ?)
相当于执行了三次插入:
INSERT INTO People VALUES('Ron', 'Obvious', 42)
INSERT INTO People VALUES('Luigi', 'Vercotti', 43)
INSERT INTO People VALUES('Arthur', 'Belling', 28)
三、验证数据是否正确插入
connection = sqlite3.connect("test_database.db")
cursor = connection.cursor()
query = "SELECT * FROM People;"
results = cursor.execute(query)
results.fetchall()
执行结果:
[('Ron', 'Obvious', 42), ('Luigi', 'Vercotti', 43), ('Arthur', 'Belling', 28)]
可以看到,三条数据都成功插入!
四、危险的字符串拼接方式
现在,假设我们要让用户输入信息并存入数据库。很多初学者会这样写:
first_name = input("Enter your first name:")
last_name = input("Enter your last name:")
age = int(input("Enter your age:"))
# 危险的字符串拼接
query = f"INSERT INTO People VALUES('{first_name}','{last_name}',{age});"
with sqlite3.connect("test_database.db") as connection:
cursor = connection.cursor()
cursor.execute(query)
这段代码有什么问题?
看起来挺合理的,对吧?让我们看看用户输入普通数据时的情况:
正常情况:用户输入 Flannery、OConnor(不带单引号)、42
生成的SQL:
INSERT INTO People VALUES('Flannery', 'OConnor', 42);
一切正常!
⚠️ 问题来了:当用户输入包含单引号
假设用户输入:
- • LastName:
O'Connor(注意这个单引号!)
拼接后的SQL:
INSERT INTO People VALUES('Flannery', 'O'Connor', 42);
数据库是怎么理解这段SQL的?
- 1. 第一个字符串:
'Flannery' ✅ 正常 - 3. 然后遇到
Connor', 42); — 这变成了裸的SQL代码,不是字符串了!
Python会很困惑:第一个引号开始,遇到第二个引号就结束了,后面的 Connor', 42);是什么鬼?
💡 小Tip:什么是SQL注入?
当用户输入的数据中包含特殊字符(如单引号),这些字符提前关闭了SQL语句中的字符串,导致后面的内容被当成SQL代码执行——这就是SQL注入的基本原理。黑客可以利用这一点执行恶意SQL命令!
五、安全的参数化查询方式
正确的写法应该是:
first_name = input("Enter your first name:")
last_name = input("Enter your last name:")
age = int(input("Enter your age:"))
data = (first_name, last_name, age)
with sqlite3.connect("test_database.db") as connection:
cursor = connection.cursor()
cursor.execute("INSERT INTO People VALUES(?, ?, ?)", data)
为什么这样写就安全了?
关键区别:数据和SQL代码被分开了!
| | |
|---|
| f"INSERT...'{name}'..." | "INSERT...?, ?, ?", data |
| | |
| | |
| | |
参数化查询的工作原理:
- 1. 数据库先收到SQL模板:
INSERT INTO People VALUES(?, ?, ?) - 3. 数据库再收到数据:
("Flannery", "O'Connor", 42) - 4. 数据库把数据原封不动地填进去,用户输入的任何特殊字符都会被自动转义
所以,当用户输入O'Connor时,数据库会自动把它转义成O''Connor(两个单引号在SQL中表示一个单引号),完美解决问题!
六、验证安全插入的结果
connection = sqlite3.connect("test_database.db")
cursor = connection.cursor()
query = "SELECT * FROM People;"
results = cursor.execute(query)
results.fetchall()
执行结果:
[('Ron', 'Obvious', 42), ('Luigi', 'Vercotti', 43), ('Arthur', 'Belling', 28), ('Flannery', 'O'Connor', 42)]
看,O'Connor被正确存储了!
七、更新数据操作
除了插入,更新数据时也要使用参数化查询:
# 将 Ron Obvious 的年龄更新为45岁
with sqlite3.connect("test_database.db") as connection:
cursor = connection.cursor()
cursor.execute("UPDATE People SET Age=? WHERE FirstName=? AND LastName=?;", (45, 'Ron', 'Obvious'))
验证更新结果:
connection = sqlite3.connect("test_database.db")
cursor = connection.cursor()
query = "SELECT * FROM People;"
results = cursor.execute(query)
results.fetchall()
执行结果:
[('Ron', 'Obvious', 45), ('Luigi', 'Vercotti', 43), ('Arthur', 'Belling', 28), ('Flannery', 'O'Connor', 42)]
Ron的年龄从42变成了45,更新成功!
八、综合练习:星际迷航船员管理系统
让我们通过一个完整的例子,巩固今天学到的知识:
import sqlite3
# 1. 创建Roster表
create_roster = """
CREATE TABLE Roster(
Name TEXT,
Species TEXT,
Age INT
);
"""
# 准备要插入的数据
roster_data = (
("Benjamin Sisko", "Human", 40),
("Jadzia Dax", "Trill", 300),
("Kira Nerys", "Bajoran", 29)
)
with sqlite3.connect("starfleet.db") as connection:
cursor = connection.cursor()
# 1. 创建表(如果存在则先删除)
cursor.execute("DROP TABLE IF EXISTS Roster")
cursor.execute(create_roster)
print("✓ 步骤1:Roster表创建成功")
# 2. 插入数据(使用executemany批量插入)
cursor.executemany(
"INSERT INTO Roster VALUES(?, ?, ?);",
roster_data
)
print("✓ 步骤2:数据插入成功")
# 查询并显示所有数据
cursor.execute("SELECT * FROM Roster;")
for row in cursor.fetchall():
print(f" 姓名:{row[0]},种族:{row[1]},年龄:{row[2]}")
# 3. 更新数据:将Jadzia Dax改为Ezri Dax
cursor.execute(
"UPDATE Roster SET Name=? WHERE Name=?;",
("Ezri Dax", "Jadzia Dax")
)
print("✓ 步骤3:姓名更新成功")
# 再次查询显示更新后的结果
cursor.execute("SELECT * FROM Roster;")
for row in cursor.fetchall():
print(f" 姓名:{row[0]},种族:{row[1]},年龄:{row[2]}")
# 4. 查询所有Bajoran人
cursor.execute(
"SELECT Name, Age FROM Roster WHERE Species='Bajoran';"
)
print("\n✓ 步骤4:Bajoran人查询结果:")
for name, age in cursor.fetchall():
print(f" 姓名:{name},年龄:{age}")
执行结果:
✓ 步骤1:Roster表创建成功
✓ 步骤2:数据插入成功
姓名:Benjamin Sisko,种族:Human,年龄:40
姓名:Jadzia Dax,种族:Trill,年龄:300
姓名:Kira Nerys,种族:Bajoran,年龄:29
✓ 步骤3:姓名更新成功
姓名:Benjamin Sisko,种族:Human,年龄:40
姓名:Ezri Dax,种族:Trill,年龄:300
姓名:Kira Nerys,种族:Bajoran,年龄:29
✓ 步骤4:Bajoran人查询结果:
姓名:Kira Nerys,年龄:29
💡 小Tip:fetchall()的返回值
注意代码中的for row in cursor.fetchall():,这里的row是一个元组,包含查询结果的一整行数据。我们可以通过索引访问每个字段:row[0]是第一个字段,row[1]是第二个字段...
而在最后的Bajoran查询中,我们直接用了for name, age in cursor.fetchall():,这是因为我们只查询了两个字段,可以直接解包赋值。
总结
今天我们学习了:
- 1. SQLite基础操作:创建表、插入数据、更新数据、查询数据
- 3. executemany批量操作:高效插入多条数据
- 4. SQL注入的原理:用户输入的单引号提前关闭SQL字符串
- 5. 参数化查询的安全性:SQL代码和数据分开传递,自动转义特殊字符
记住一条黄金法则:永远不要用字符串拼接的方式构造SQL语句,始终使用参数化查询!
今天的Python学习就到这里,下次再见!
📦 资源获取提示
关注「码农自习室」,后台回复关键词 Python学习,即可获取本文完整代码及配套练习数据集,一起动手掌握高效数据操作的核心技巧!
❤️ 支持我们
如果觉得本文对你有帮助,欢迎点赞 + 推荐 + 关注,您的支持是我们持续创作优质内容的最大动力!
📚 学习资源说明
本文内容整理自《Python基础教程(第3版)》第15章,所有命令均已实测。
让我们一起坚持学习,每天进步一点点!💪