Python MySQL 驱动中防止 SQL 注入的写法

SQL 注入长期位居 OWASP Top 10 前列,对于使用 Python 操作 MySQL 的开发者来说,理解不同驱动下的安全写法,是构建安全 Web 应用的基本功。本文将从 SQL 注入的成因出发,系统梳理 Python 主流 MySQL 驱动中防止注入的正确写法与常见误区。

SQL 注入的本质

SQL 注入的核心原因只有一个:用户输入被当作 SQL 代码执行了。当开发者用字符串拼接的方式构造 SQL 语句时,攻击者可以通过精心构造的输入改变原有 SQL 的语义。

一个典型的错误写法:

# 危险!不要这样写
user_id = request.args.get("id")
sql = f"SELECT * FROM users WHERE id = {user_id}"
cursor.execute(sql)

如果攻击者传入 1 OR 1=1,SQL 就变成了 SELECT * FROM users WHERE id = 1 OR 1=1,整张表的数据都会被返回。更严重的情况下,攻击者可以通过 UNION SELECT 读取其他表的数据,甚至通过堆叠查询执行 DROP TABLE 等破坏性操作。

理解这一点之后,防御思路就很清晰了:让用户输入永远以“数据”的身份参与查询,而不是以“代码”的身份。参数化查询(Prepared Statement)正是实现这一目标的标准手段。

参数化查询:通用原则

参数化查询的工作机制是:先向数据库发送带有占位符的 SQL 模板,数据库完成语法解析和查询计划生成;然后再单独发送参数值,数据库将参数值作为纯数据填入。由于参数在 SQL 解析阶段并不参与语法构建,注入攻击从根本上失去了作用空间。

在 Python 中,几乎所有 MySQL 驱动都支持参数化查询,区别只在于占位符的写法。下面分别说明。

mysql-connector-python

这是 MySQL 官方提供的 Python 驱动,使用 %s 作为占位符:

import mysql.connector

conn = mysql.connector.connect(
    host="localhost", user="app", password="secret", database="mydb"
)
cursor = conn.cursor()

# 安全写法
user_id = request.args.get("id")
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
result = cursor.fetchall()

# 插入同样安全
cursor.execute(
    "INSERT INTO users (name, email) VALUES (%s, %s)",
    (name, email)
)
conn.commit()

注意两点:第一,即使只有一个参数,也必须传元组,即 (user_id,) 而不是 user_id,否则会报类型错误;第二,%s 只是占位符,不要加引号写成 '%s',驱动会自动处理类型。

PyMySQL

PyMySQL 的 API 与 mysql-connector 高度一致,同样使用 %s

import pymysql

conn = pymysql.connect(host="localhost", user="app",
                       password="secret", database="mydb")
with conn.cursor() as cursor:
    cursor.execute("SELECT * FROM users WHERE name = %s", (name,))
    rows = cursor.fetchall()
conn.commit()

PyMySQL 内部会对参数进行转义处理,但不要依赖 conn.escape() 手动转义,参数化查询才是正确路径。

mysqlclient(MySQLdb)

mysqlclient 是 MySQLdb 的维护分支,性能较好,Django 默认使用它。占位符同样是 %s

import MySQLdb

conn = MySQLdb.connect(host="localhost", user="app",
                       passwd="secret", db="mydb")
cursor = conn.cursor()
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))

SQLAlchemy:ORM 与 Core 两种写法

SQLAlchemy 是 Python 生态中最主流的 ORM。使用 ORM 时,查询条件通过方法链或 select() 构造,天然参数化:

# ORM 写法,安全
user = session.query(User).filter(User.name == name).first()

# 2.0 风格
stmt = select(User).where(User.name == name)

使用 SQLAlchemy Core 执行原生 SQL 时,必须使用 text() 配合 bindparams

from sqlalchemy import text

# 安全
stmt = text("SELECT * FROM users WHERE id = :uid")
result = conn.execute(stmt, {"uid": user_id})

# 危险!不要用字符串拼接
stmt = text(f"SELECT * FROM users WHERE id = {user_id}")

SQLAlchemy 使用命名参数 :name 而非 %s,这是它与其他驱动的一个显著区别。

常见误区与危险写法

即便知道参数化查询,实际开发中仍有不少容易踩的坑:

1. 表名、列名无法参数化

参数化查询只能用于“值”,不能用于标识符。如果排序字段来自用户输入,不能写成 ORDER BY %s,而应使用白名单校验:

ALLOWED_SORT = {"id", "name", "created_at"}
sort = request.args.get("sort", "id")
if sort not in ALLOWED_SORT:
    sort = "id"
sql = f"SELECT * FROM users ORDER BY {sort}"

2. 用 format% 拼接

# 危险
cursor.execute("SELECT * FROM users WHERE name = '%s'" % name)
cursor.execute("SELECT * FROM users WHERE name = '{}'".format(name))

这两种写法虽然用了 %s{},但它们是 Python 层面的字符串格式化,不是数据库参数化,注入依然存在。

3. LIKE 查询的误用

# 安全
cursor.execute(
    "SELECT * FROM users WHERE name LIKE %s",
    (f"%{keyword}%",)
)

参数值内部包含 % 是允许的,因为它是作为数据传入的。

4. 依赖转义函数

conn.escape_string() 之类的方法在某些字符集或边界情况下可能失效,不应作为主要防御手段。

纵深防御:不止于参数化

参数化查询是防注入的第一道也是最关键的一道防线,但安全加固不应止步于此:

  • 最小权限原则:应用连接数据库的账号只授予必要的库表权限,禁用 DROPFILE 等高危权限,这样即使被注入,破坏范围也有限。
  • 输入校验:对 ID、页码等强类型参数做类型转换和范围校验,不符合预期直接拒绝。
  • 错误信息收敛:生产环境不要将数据库原始报错返回给前端,避免泄露表结构。
  • 统一数据访问层:封装 DAO 层,禁止业务代码直接拼接 SQL,从工程层面降低出错概率。

小结

Python 操作 MySQL 时,防止 SQL 注入的核心原则是始终使用参数化查询:mysql-connector-python、PyMySQL、mysqlclient 使用 %s 占位符,SQLAlchemy 使用 :name 命名参数。表名、列名等无法参数化的位置,必须通过白名单校验。同时配合最小权限、输入校验和错误收敛,构建多层防御体系。牢记一句话:永远不要用字符串拼接构造 SQL

未经允许不得转载:任鹏个人博客 » Python MySQL 驱动中防止 SQL 注入的写法

赞 (0) 打赏

评论 0

取消
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏