参数化查询和orm能拦截多数sql注入执行异常,但“不报错”不等于安全;错误被吞没、权限绕过或数据静默篡改更危险,因单引号、分号、--等可致语法断裂或逻辑反转,且不同驱动占位符混用、orm误用text()或动态字段名未白名单校验均会失效。

直接用参数化查询或 ORM 就能拦住绝大多数 SQL 注入导致的执行异常,但光靠“不报错”不等于安全——错误被吞掉、权限被绕过、数据被静默篡改,才是真正危险的地方。
为什么字符串拼接会触发数据库执行异常
不是所有拼接都会立刻报错,但一旦用户输入里含单引号、分号、注释符(如 --)、括号或关键字(如 UNION),就可能让 SQL 语法断裂或逻辑反转。比如:
-
username = "admin' OR '1'='1"→ 拼成WHERE username = 'admin' OR '1'='1',查出所有用户 -
id = "1; DROP TABLE users;"→ 在某些驱动+配置下真会执行多语句,引发OperationalError或更糟的静默删表 -
search = "%' AND 1=1 -- "→ 可能让后续条件失效,返回不该返回的数据
这些异常不总体现为 Python 报错,有时只是结果集异常膨胀或为空,排查时容易误判为业务逻辑问题。
不同数据库驱动的参数化语法不能混用
占位符写错会导致参数没被绑定,退化成字符串拼接——看着像防注入,实则裸奔。
- SQLite 和 pymysql 用
?或%s,但%s在 SQLite 里不被识别,会抛sqlite3.OperationalError - psycopg2(PostgreSQL)必须用
%s,写成?会原样传入,变成字面量字符串,查不到数据也不报错 - SQLAlchemy 的
text()如果手动拼接变量,:param命名占位符照样失效,得用bindparam()或**params
验证方法:打开数据库日志,或在 cursor.execute() 后打印 cursor._last_executed(MySQL)或启用 echo=True(SQLAlchemy),看最终发出去的 SQL 是否含用户原始输入。
ORM 并非绝对免疫,filter() 里藏 eval 风险
SQLAlchemy、Django ORM 天然防注入,但开发者自己“破防”的情况很常见:
- 用
filter(User.name == f"%{keyword}%")—— keyword 里有%或_会干扰 LIKE 语义,不是注入,但结果错乱 - 用
filter(text("name = '{}'".format(keyword)))—— 直接回到石器时代,text()不自动转义 - 用
order_by(request.args.get('sort'))动态字段名 —— 字段名不能参数化,必须白名单校验,否则sort=id; DROP TABLE--可能进 SQL
字段名、表名、排序方向、LIMIT 数值这些“元信息”,参数化接口不支持,必须走严格白名单或正则校验(如 re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', field))。
异常处理别吞掉关键上下文
捕获 DatabaseError 时如果只打一行 logger.error("DB failed"),就丢掉了攻击痕迹。真实攻防中,SQL 错误信息(尤其是 PostgreSQL 的 DETAIL、HINT)常暴露表结构、列名甚至数据内容。
- 开发环境可开
echo=True+ 记录完整 SQL,但生产必须关 - 对用户返回泛化提示(如“操作失败,请稍后重试”),绝不返回原始
str(exc) - 把
exc.__class__.__name__、SQL 片段(截断前 100 字)、绑定参数类型(如是否含'或;)记进审计日志
最易忽略的是:即使用了参数化,当输入是空字符串、None、超长文本或控制字符时,仍可能触发数据库层面的约束异常(如 NOT NULL、unique 冲突),这类异常和注入无关,但日志里若不区分,会掩盖真正的攻击尝试。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











