直接在数据库驱动层使用参数化查询是防止 SQL 注入最有效的方法,适用于所有基于 SQL 数据库的 Python Web 项目。
核心建议:严禁拼接 SQL 字符串,必须使用数据库驱动提供的占位符机制传递参数,这是消除注入风险的根本手段。
- 检查:搜索代码中是否存在 f-string、format 或 + 号拼接 SQL 语句的情况。
- 修改:将拼接变量改为驱动支持的占位符(如 %s 或 ?),并将数据作为独立参数传入。
- 验证:使用包含单引号、分号等特殊字符的输入测试接口,确认不会报错或执行意外逻辑。
完整代码示例
以下基于 Python 内置的 sqlite3 模块演示,包含连接建立、错误写法对比及正确写法。其他驱动(如 psycopg2、pymysql)逻辑一致,仅占位符风格略有不同。
import sqlite3
# 建立连接
conn = sqlite3.connect(':memory:')
cursor = conn.cursor()
# 初始化表结构
cursor.execute('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)')
cursor.execute("INSERT INTO users (name) VALUES ('Alice')")
conn.commit()
# --- 错误写法(存在注入风险) ---
user_input = "' OR '1'='1"
# 危险:直接拼接字符串
sql_risk = "SELECT * FROM users WHERE name = '" + user_input + "'"
# cursor.execute(sql_risk) # 执行会导致注入
# --- 正确写法(参数化查询) ---
# 安全:使用占位符 ?,sqlite3 使用问号,MySQL/PG 通常使用 %s
sql_safe = "SELECT * FROM users WHERE name = ?"
cursor.execute(sql_safe, (user_input,))
# 获取结果
result = cursor.fetchall()
print(f"查询结果数量:{len(result)}") # 参数化后结果为 0,注入被拦截
# 关闭资源
cursor.close()
conn.close()验证方法
1. Payload 测试
在输入框尝试提交 ' OR '1'='1 或 '; DROP TABLE users; -- 等经典 payload。如果接口返回正常数据(即未匹配到数据)且数据库未受损,说明注入被拦截。参数化查询会将输入内容视为纯文本值,而非 SQL 指令。
2. 代码审查
确认所有 execute 调用都没有使用字符串格式化操作。如果使用 ORM(如 SQLAlchemy、Django ORM),默认情况下它们已经使用了参数化查询,但需注意 extra() 或 raw() 等绕过机制。
常见注意事项
1. 表名和字段名无法参数化
参数化查询只能用于值(WHERE 后面的内容),不能用于表名、字段名或 ORDER BY 子句。这些位置如果需要动态控制,必须使用白名单校验,不能直接信任用户输入。
2. 占位符不要加引号
驱动会自动处理数据类型和转义。例如 WHERE name = ? 是正确的,而 WHERE name = '?' 会导致查询失败或类型错误。
3. 参数传递格式
确保第二个参数是序列类型(如元组或列表)。如果只有一个参数,记得加逗号构成元组,例如 (value,) 而不是 (value)。
参考来源
- OWASP SQL Injection Prevention Cheat Sheet, https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html
- Python Database API Specification v2.0 (PEP 249), https://peps.python.org/pep-0249/