SQL注入本质是动态拼接而非输入本身危险,防御核心是切断拼接路径,禁用字符串拼接SQL,强制参数化,并在数据库驱动层拦截原始SQL,对无法参数化的表名、排序字段等严格白名单校验。

SQL注入的本质是拼接,不是输入本身
所有所谓“危险输入”只有在参与动态SQL拼接时才真正危险。用户提交的 ' OR 1=1 -- 没有被 query 函数直接拼进字符串里,它就只是普通字符串。防御的第一步不是过滤字符,而是切断拼接路径。
- 永远不用
string.format、+或模板字符串拼接 SQL(Python/JS/Java 都适用) - ORM 的
.filter()、.where()方法默认参数化,但一旦写成.raw('SELECT * FROM u WHERE name = \'' + name + '\''),立刻失效 - 存储过程里用
EXEC(@sql)或sp_executesql时,若传入的 @sql 是拼出来的,参数化也救不了
统一中间件必须绕过 ORM 自带的“假安全”陷阱
很多团队加了中间件,却仍被注入攻破,原因往往是 ORM 层的“自动转义”被误信。Django 的 extra()、SQLAlchemy 的 text()、MyBatis 的 ${} 占位符,全都不受参数化保护。
- 中间件应拦截所有进入数据库驱动前的原始 SQL 字符串,而非只检查 HTTP 请求体
- 对
cursor.execute(sql, params)这类调用,中间件需确保sql中不含用户可控变量 —— 若含,直接抛出SecurityError: raw SQL with interpolation detected - 禁止中间件做 HTML 实体转义或正则删
UNION关键字:这类规则极易被绕过,且破坏合法数据(比如用户名就是O'Reilly)
中间件的注册点决定它是否真正生效
在 Web 框架路由之后、业务逻辑之前注册中间件,等于没防。SQL 注入常发生在登录校验、搜索、导出等非主流程中,这些地方可能绕过常规中间件链。
- 最佳注册点是数据库驱动封装层,例如重写
psycopg2.connect()返回的cursor,或 monkey patchmysql.connector.cursor.execute() - 若用连接池(如
SQLAlchemy.create_engine(pool_pre_ping=True)),中间件必须作用于每个新生成的cursor,不能只 hook 初始化时的 engine - CLI 脚本、定时任务、后台 worker 往往不走 Web 中间件,它们的数据库调用必须复用同一套驱动层防护逻辑
参数化不是万能解,边界场景必须人工确认
表名、列名、ORDER BY 字段、LIMIT 数值 —— 这些无法用占位符参数化的部分,是中间件最难兜底的地方。靠白名单或枚举映射是唯一可行方案,没有捷径。
- 遇到
ORDER BY ?报错(SQLite/MySQL 不支持参数化排序字段),必须提前将用户输入映射为预设值:{'created': 'created_at', 'name': 'user_name'} -
LIMIT ? OFFSET ?在多数驱动中合法,但若用户传负数或超大整数,可能引发 DoS,中间件需额外校验数值范围 - 动态构建多租户查询时(如
SELECT * FROM tenant_{id}.users),{id} 必须用正则^[a-z0-9_]{3,16}$严格校验,不能只 check isdigit()
最易被忽略的一点:日志里记录 SQL 时如果把 params 和 sql 分开打,攻击者可能通过错误页面或日志泄露反推参数化是否真实生效。中间件若做 SQL 审计,必须记录拼接后的完整语句(脱敏后),否则等于盲审。

















