Python操作数据库怎么防止SQL注入攻击
在Python开发中,防止SQL注入攻击的核心手段只有一个:使用数据库驱动原生提供的参数化查询(Parameterized Query),绝对不要使用字符串格式化直接拼接SQL语句。无论是使用Python标准库自带的sqlite3,还是第三方的PyMySQL、psycopg2以及各类ORM框架,参数化查询都能保证外部传入的内容被数据库引擎完全视作普通数据字面量,而不会改变原有的SQL语法树结构。
一、SQL注入攻击的成因与底层防御原理
SQL注入的根源在于“指令与数据未作清晰分离”。当开发者使用字符串拼接(例如 f-string、+ 运算符或 str.format())来构造SQL语句时,数据库引擎接收到的是一段混合在一起的文本。此时如果用户输入包含了单引号、注释符或SQL关键字,数据库解释器就会将这些恶意输入当成SQL语法指令的一部分来解析,从而改变查询的原本语义。
参数化查询(也称为预编译语句绑定机制)从协议层面解决了这个问题。客户端驱动首先将包含占位符的SQL模版发送至数据库或者驱动解析器,完成语法结构的分析与编译;随后将用户传入的具体参数作为独立的数据载荷传输。即使参数中包含 ' OR '1'='1 或 -- 等字符,数据库也只会将其作为字段内容进行比对,不可能跃升为语法指令。
二、主流Python数据库驱动的标准参数化操作步骤
Python制定了通用的数据库API规范(PEP 249),不同的数据库驱动在占位符符号上有所区别。在编写查询时,必须根据驱动指定的规范传入占位符和参数序列。
1. SQLite (标准库 sqlite3)
在 sqlite3 中,默认使用问号 ? 作为位置占位符。参数必须通过元组(tuple)或列表(list)作为 execute() 的第二个参数传入。
import sqlite3
conn = sqlite3.connect("app.db")
cursor = conn.cursor()
user_input = "zhangsan"
# 正确做法:使用 ? 作为占位符,参数放在元组中传入
sql = "SELECT id, username, email FROM users WHERE username = ? AND status = ?"
cursor.execute(sql, (user_input, 1))
rows = cursor.fetchall()
# 错误做法(绝对禁止):使用 f-string 或 % 拼接
# sql = f"SELECT id, username FROM users WHERE username = '{user_input}'"
# cursor.execute(sql)
(user_input,)。如果写成 (user_input),Python会将其识别为普通字符串括号,导致字符串被当成字符序列解包拆分,引发参数数量不匹配的报错。
2. MySQL (PyMySQL / mysql-connector-python)
在 PyMySQL 等MySQL驱动中,遵循 format 或 pyformat 风格,占位符为 %s。这常常让初学者产生困惑,误以为这是Python自带的字符串格式化。实际上,这里的 %s 只是SQL语句中的标记,真正的值是通过驱动的独立参数传入的。
import pymysql
connection = pymysql.connect(host="localhost", user="root", password="pwd", database="test")
with connection.cursor() as cursor:
user_input = "lisi"
# 正确做法:驱动内部安全转义,不要给 %s 加单引号
query = "SELECT id, role FROM accounts WHERE username = %s"
cursor.execute(query, (user_input,))
result = cursor.fetchone()
# 错误做法:在SQL模板中给 %s 加单引号:'%s' 这是错误的写法!
# query_wrong = "SELECT id FROM accounts WHERE username = '%s'"
三、动态表名、列名与排序字段的安全处理
许多开发者在遇到“动态排序(ORDER BY)”或“动态表名/列名查询”时容易误入歧途。SQL标准的参数化占位符只能替代数据值(例如 WHERE 条件中的数值或字符串、INSERT 中的 VALUES),不能用于表名、列名、操作符或排序方向(ASC/DESC)。
如果业务确实需要用户动态选择排序列或查询特定表,绝不能直接将外部字符串拼进SQL。此时正确的防范策略是:采用服务端严格白名单校验(Whitelist Mapping)。
def get_sorted_users(cursor, sort_column: str, direction: str):
# 1. 声明服务端允许的合法字段白名单字典
ALLOWED_COLUMNS = {
"created_at": "created_at",
"username": "username",
"score": "score"
}
# 2. 声明排序方向白名单
ALLOWED_DIRECTIONS = {
"asc": "ASC",
"desc": "DESC"
}
# 3. 校验并映射为受控的安全标识符
safe_column = ALLOWED_COLUMNS.get(sort_column.lower())
safe_direction = ALLOWED_DIRECTIONS.get(direction.lower())
if not safe_column or not safe_direction:
raise ValueError("非法的排序字段或排序方向!")
# 4. 标识符来自内部白名单常量,值采用参数化查询,双重保障
sql = f"SELECT id, username, created_at FROM users WHERE status = ? ORDER BY {safe_column} {safe_direction}"
cursor.execute(sql, (1,))
return cursor.fetchall()
通过白名单字典的映射,无论攻击者传入 id; DROP TABLE users;-- 还是复杂的盲注语句,在字典匹配阶段都会直接失败,完全无法进入后续SQL组装环节。
四、ORM框架与原生SQL混用场景的注意事项
现代Python工程常采用ORM(对象关系映射)框架,如 SQLAlchemy、Django ORM 或 Peewee。ORM底层自动封装了参数化查询,通过其模型API进行的常规增删改查天然具备防注入能力:
# Django ORM: 默认完全参数化,安全
User.objects.filter(username=user_input, is_active=True)
# SQLAlchemy 2.0 ORM: 默认完全参数化,安全
stmt = select(User).where(User.username == user_input)
result = session.scalars(stmt).all()
但在实际业务中,当开发者需要编写复杂报表或特定优化查询而使用“原生SQL片段”时,注入漏洞极易复发。例如:
text() 时直接拼接字符串:session.execute(text(f"SELECT * FROM users WHERE name = '{user_input}'")) # 存在注入漏洞!
在使用 text() 或原生SQL接口时,必须显式使用冒号命名参数绑定:
from sqlalchemy import text
# 正确:在 text() 中使用冒号命名占位符,并通过字典或命名实参传参
stmt = text("SELECT id, username, balance FROM users WHERE username = :uname AND status = :st")
result = session.execute(stmt, {"uname": user_input, "st": 1})
rows = result.fetchall()
在Django中使用 raw() 执行原生SQL时同样要使用参数列表:
# Django raw() 正确用法:占位符与参数列表分离
User.objects.raw("SELECT * FROM myapp_user WHERE username = %s", [user_input])
五、防御规范全景与安全检查清单
要建立稳固的数据库安全防护体系,开发团队应当将以下准则纳入代码审查与研发规范:
总结来说,防范SQL注入不是依靠不可靠的黑名单字符过滤或正则表达式清洗,而是依靠体系化的代码约束:数据值用参数化占位符,结构标识符用内部白名单映射,高层ORM严禁拼接原生SQL。只要坚持遵循参数化查询原则,SQL注入攻击在Python项目中就可以得到彻底根除。
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









