SQL盲注与参数化查询无关,其利用逻辑可探测性而非语句结构篡改;ORM安全依赖全程不拼接、动态字段白名单校验及text()配合bindparam()绑定,业务层控制响应差异才是关键防线。

SQL盲注和参数化查询不是一回事
盲注能成功,不代表你没做参数化——它只说明攻击者没看到报错或数据回显,但依然能靠布尔响应或时间延迟猜出数据。SQLAlchemy 2 的 filter()、filter_by() 和 text() + 参数字典本身不防盲注,它们防的是语句结构被篡改;而盲注利用的是“逻辑可探测性”,比如 WHERE id = ? AND (SELECT SUBSTR(password,1,1) FROM users WHERE id=1) = 'a' 这种判断式查询,只要后端返回真假差异(哪怕只是页面空白/非空白),就可能被利用。
ORM 查询必须全程不拼接、不绕过绑定机制
常见翻车点是看似用了 ORM,实则在关键环节退化为字符串操作:
-
filter(User.name.like(f"%{q}%"))❌ ——f""在 Python 层已拼出完整 LIKE 模式,like()不接管参数化 -
filter(text("name = '" + name + "'"))❌ —— 直接把拼好的字符串喂给text(),等于放弃所有防护 -
order_by(request.args.get("sort"))❌ ——ORDER BY字段无法参数化,必须白名单校验 -
getattr(User, request.args.get("field"))❌ —— 动态列名未校验,request.args.get("field")可传__table__.metadata或其他危险属性
真正安全的写法是:filter(User.name.like("%" + q + "%")) ✅(注意:这是 SQLAlchemy 2 的行为,+ 被重载为 SQL 表达式拼接,不是 Python 字符串拼接);或统一走 bindparam() + text()。
用 text() 时必须配 bindparam() 和命名占位符
text() 是安全边界断裂点,但不是不能用——关键是所有用户可控值都得走绑定,且只允许 :name 形式占位符:
- ❌ 错误:
text("SELECT * FROM users WHERE name = %s")(%s是 Python 格式化,不是 SQL 占位符) - ❌ 错误:
text("WHERE name = :name").params(name=q)(.params()不触发执行期绑定,易被绕过) - ✅ 正确:
text("SELECT * FROM users WHERE name = :name AND status = :status")+session.execute(stmt, {"name": q, "status": "active"}) - ✅ 动态排序(需白名单):
text("SELECT * FROM users ORDER BY :sort_field").bindparams(bindparam("sort_field", value=validated_sort))
注意:bindparam() 必须显式指定 value=,否则只声明参数名,不传入实际值。
盲注防御真正起作用的其实是业务层控制
SQLAlchemy 本身不提供“防盲注”开关,最终防线在你如何设计接口逻辑:
- 避免返回布尔型差异:比如“用户存在/不存在”直接返回 404 或通用提示,而不是靠页面是否渲染某块内容来区分
- 限制单次请求可探测的字段数:对模糊搜索、分页、排序等高频盲注入口加请求频率限制或 token 校验
- 敏感字段绝不参与条件推断:不要在 WHERE 中用
password、email_token等字段做等值判断并暴露响应差异 - 日志里不记录原始 SQL 或参数值,防止攻击者从错误日志反推结构
最常被忽略的一点:text() 查询如果带 UNION SELECT 或子查询,即使参数化了,只要业务逻辑允许返回任意字段组合,盲注探测成本就极低——这不是 SQLAlchemy 的问题,是你没约束查询意图。











