text()本身不防注入,仅靠params传参才安全;必须用字典或元组传值且sql含:name等占位符,驱动层才预编译;动态字段名等元信息须白名单校验,严禁拼接用户输入。

text()本身不防注入,只靠params传参才安全
很多人以为调用text()就自动免疫SQL注入,其实它只是个SQL字符串包装器,不校验内容、不拦截拼接。真正起作用的是params参数——必须用字典或元组传值,且SQL里得有:name或:1这类占位符,驱动层才会走预编译。
常见错误现象:sqlalchemy.exc.ProgrammingError: (psycopg2.errors.UndefinedColumn) column "admin'--" does not exist,说明后端把用户输入当字段名解析了;更危险的是syntax error at or near "OR",代表' OR 1=1 --已被原样塞进SQL执行。
- ✅ 安全写法:
conn.execute(text("SELECT * FROM users WHERE email = :email"), {"email": user_input}) - ❌ 危险写法:
conn.execute(text(f"SELECT * FROM users WHERE email = '{user_input}'")) - ⚠️ 注意:
:email里的冒号不能漏,PostgreSQL/MySQL驱动都依赖这个标记识别参数位置
动态字段名(如order_by、GROUP BY)必须白名单校验
占位符只能绑定值,不能绑定字段名、表名、操作符(如ASC/DESC)。一旦允许用户控制这些元信息,text()立刻失效。
使用场景:前端传sort=name、group=category、table=report_2024等。
- 排序字段必须映射到固定列表:
{"name": "users.name", "date": "users.created_at"}.get(sort_param, "id") - 字段名校验示例:
if sort_param not in ["name", "email", "created_at"]: raise ValueError("Invalid sort field") - 表名严禁用
globals()[table_name]或getattr(models, table_name),应硬编码映射:{"user": User, "post": Post}
别碰literal_column()和bindparam()的误用陷阱
literal_column()是显式告诉SQLAlchemy“这段字符串就是SQL代码”,等于主动放弃参数化保护。而bindparam()本该用于Core查询,但在text()里混用容易误导——它不改变text()字符串本身,只影响后续绑定逻辑。
典型翻车:text("SELECT * FROM users ORDER BY " + literal_column(request.args.get("sort"))),攻击者传sort=id; DROP TABLE users--直接触发堆叠注入。
- ✅ 正确做法:字段名走白名单,再拼进
text()字符串(此时已无用户输入) - ✅ 更安全替代:
select(users_table).order_by(users_table.c[name_col]),用Python对象定义结构 - ⚠️
bindparam("col")在text()里没意义,它只对select().where()等Core构造生效
raw()和extra()不是ORM安全区,而是高危接口
Django的raw()、extra(),Flask-SQLAlchemy的session.execute(text(...)),表面看是“高级功能”,实则是SQL注入温床——它们绕过ORM抽象,直连SQL生成链路。
性能影响几乎为零:用params传参和ORM普通查询一样走预编译缓存;但手拼SQL每次都要重新解析、计划,还可能触发数据库语法错误。
- 永远禁用
.format()、f""、+拼接SQL片段 - 搜索框模糊匹配也得参数化:
text("SELECT * FROM users WHERE name ILIKE :q"),而不是f"name ILIKE '%{q}%'" - 如果必须动态拼接,先做正则过滤:
re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", field_name),但白名单仍优于正则
复杂点在于:字段名、表名、排序方向这些“元信息”无法参数化,必须在SQL生成前就卡死。很多团队只盯着params传参,却放行了request.args.get("field"),结果防线从最外层就崩了。











