修复 SQL 注入的核心是权限控制而非输入过滤,必须用参数化查询隔离 SQL 结构与数据,动态表名 / 字段名需白名单硬编码,ORM 原始查询接口仍需警惕。

SQL 注入不是拼字符串的问题,是执行权限失控
修复 SQL 注入逻辑错误的核心,不是“把用户输入过滤掉”,而是让数据库根本不允许动态拼接的 SQL 获得执行权限。所有用 string + user_input 拼出完整查询语句的做法,无论加不加转义、正则、黑名单,都属于权限模型崩塌——你本不该让应用层决定“这条 SQL 要不要跑”。
必须用参数化查询,且不能绕过预编译阶段
参数化查询之所以有效,是因为数据库驱动在发送语句前就将 SQL 结构(含占位符)和参数值分开传输,服务端严格区分“语法”和“数据”。常见错误是看似用了参数,实则仍拼接:
- ❌ 错误:用
format()或f-string拼接表名 / 字段名,只对值做参数化 —— 表名无法参数化,这是语法层级,不是数据层级 - ❌ 错误:用
mysql.connector的%s占位符,但传入的是拼好的字符串(如"'admin' OR 1=1")—— 参数化只防注入,不防恶意值本身 - ✅ 正确:所有用户可控部分(WHERE 条件值、INSERT 值、ORDER BY 字段值)必须走驱动原生参数绑定,如 Python 的
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
动态表名 / 字段名必须白名单硬编码
当业务真需要根据用户输入切换表或字段(如多租户分表、可配置报表),不能靠“过滤”或“转义”,只能提前定义合法范围,并用字典 / 枚举映射:
- 用字典查表:
table_map = {"orders_2024": "orders_q1", "orders_2023": "orders_legacy"},用户传"orders_2024"→ 查得"orders_q1"→ 拼进 SQL;传任何非法键直接报错 - 字段排序也同理:
allowed_sorts = {"name": "user_name", "time": "created_at"},用户传sort="name"→ 映射为"user_name"→ 拼进ORDER BY - 注意:白名单必须是代码里写死的字符串,不能从数据库或配置文件动态加载(除非该配置文件本身不可被用户修改)
ORM 不等于安全,raw() 和 extra() 是高危接口
Django、SQLAlchemy 等 ORM 默认启用参数化,但一旦调用底层原始查询接口,就退回到手写 SQL 的风险模型:
-
Model.objects.raw("SELECT * FROM users WHERE name = %s", [name])安全(Django 支持参数化 raw) -
Model.objects.extra(where=["name = '%s'" % name])❌ 危险,字符串拼接发生在 Python 层,参数化失效 -
session.execute(text("SELECT * FROM users WHERE name = :name"), {"name": name})✅ 安全(SQLAlchemy 的text()+ 参数字典) - 检查项目中所有
raw、extra、execute、query调用点,确认是否真正隔离了结构与数据
最难处理的其实是“权限粒度”问题:一个接口既要支持模糊搜索,又要按时间范围筛选,还要允许用户选字段导出——这种组合式动态查询,很容易在“方便”和“安全”之间滑向后者。白名单 + 参数化不是银弹,它要求你在设计阶段就明确哪些动态是真必要,哪些只是偷懒。