一、ORM 注入的真相
很多开发者以为用了 ORM 就自动防注入了。实际上,几乎每个主流 ORM 框架都有可以注入的入口——而且这些入口经常被开发者主动使用(因为"方便")。
本文覆盖 Python SQLAlchemy、Node.js Sequelize、Go GORM、Django ORM 四大框架的注入面。
二、Python SQLAlchemy 注入
2.1 text() 原始 SQL
from sqlalchemy import create_engine, text
engine = create_engine("mysql+pymysql://root:root@localhost/shop")
# ❌ 危险:text() 里拼接字符串
username = request.args.get('username')
sql = f"SELECT * FROM users WHERE username = '{username}'"
result = engine.execute(text(sql))
# ❌ 危险:text() 用 % 格式化
sql = "SELECT * FROM users WHERE username = '%s'" % username
result = engine.execute(text(sql))
# ✅ 安全:text() 用参数绑定
sql = "SELECT * FROM users WHERE username = :username"
result = engine.execute(text(sql), {"username": username})
2.2 filter 中的原始字符串
from models import User
# ❌ 危险:filter 里用字符串表达式
username = request.args.get('username')
users = User.query.filter(f"User.username == '{username}'").all()
# ❌ 危险:用 text() 在 filter 中
users = User.query.filter(text(f"username = '{username}'")).all()
# ✅ 安全:ORM 对象 API
users = User.query.filter(User.username == username).all()
2.3 literal_column 和 cast
from sqlalchemy import literal_column, cast, String
# ❌ 危险:literal_column 里拼接
order_col = request.args.get('order')
stmt = select(Product).order_by(literal_column(f"{order_col} ASC"))
# ❌ 危险:cast 里拼接
value = request.args.get('val')
stmt = select(Product).where(cast(Product.name, String) == value)
# ✅ 安全:白名单 + 对象 API
ALLOWED_ORDERS = ['id', 'name', 'price']
if order_col in ALLOWED_ORDERS:
stmt = select(Product).order_by(getattr(Product, order_col).asc())
2.4 SQLAlchemy 完整危险 API 列表
| API | 风险等级 | 说明 |
|---|---|---|
engine.execute(text(sql)) |
极高 | 原始 SQL,拼接就危险 |
connection.execute(sql) |
极高 | 同上 |
Model.query.filter(text(sql)) |
极高 | 在 filter 中用 text |
Model.query.filter("...") |
极高 | 字符串 filter |
select().where(text(sql)) |
极高 | 查询中用 text |
literal_column() |
高 | 用字符串创建列引用 |
column() |
中 | 如果传入字符串也危险 |
Table.__table_args__ 中的 CheckConstraint |
低 | 静态定义,运行时不拼接 |
三、Node.js Sequelize 注入
3.1 raw query
const { Sequelize } = require('sequelize');
const sequelize = new Sequelize('shop', 'root', 'root', { dialect: 'mysql' });
// ❌ 危险:sequelize.query 默认字符串拼接
const username = req.body.username;
const [results] = await sequelize.query(`SELECT * FROM users WHERE username = '${username}'`);
// ❌ 危险:也会拼接
const [results2] = await sequelize.query("SELECT * FROM users WHERE username = '" + username + "'");
// ✅ 安全:参数绑定
const [results3] = await sequelize.query(
"SELECT * FROM users WHERE username = ?",
{ replacements: [username] }
);
// ✅ 安全:命名参数
const [results4] = await sequelize.query(
"SELECT * FROM users WHERE username = :user",
{ replacements: { user: username } }
);
3.2 where 对象的陷阱
// ❌ 危险:Op.like 拼接
const products = await Product.findAll({
where: {
name: { [Op.like]: `%${req.query.q}%` } // 这个其实是安全的!
}
});
// 实际上 Sequelize 的 Op.like 会正确转义
// 但如果你用 raw 方法:
const products2 = await Product.findAll({
where: sequelize.literal(`name LIKE '%${req.query.q}%'`) // ❌ 危险!
});
// ✅ 安全:保持在对象 API 内
const products3 = await Product.findAll({
where: {
name: { [Op.like]: `%${req.query.q}%` }
}
});
3.3 order/limit 中的注入
// ❌ 危险:order 传入字符串
const orderCol = req.query.order;
await Product.findAll({ order: orderCol });
// 攻击者可以传: order=id; DROP TABLE users;--
// ✅ 安全:白名单
const ALLOWED_ORDERS = ['id', 'name', 'price', '-id', '-name', '-price'];
if (ALLOWED_ORDERS.includes(orderCol)) {
await Product.findAll({ order: orderCol });
}
// ✅ 安全:嵌套数组 + 对象 API
await Product.findAll({ order: [['id', 'DESC']] });
3.4 Sequelize 原生 where 扩展
// ❌ 危险:Sequelize.where 的 whereItem 如果用字符串
const where = Sequelize.where(
Sequelize.col('name'),
'=',
req.query.name // 这个是安全的,因为用了 = 运算符
);
// 但如果用 raw 字符串
const where2 = Sequelize.where(
Sequelize.literal(`name = '${req.query.name}'`),
true
); // ❌ 危险!
四、Go GORM 注入
4.1 Raw 和 Exec
import (
"gorm.io/gorm"
"gorm.io/driver/mysql"
)
db, _ := gorm.Open(mysql.Open("root:root@tcp(localhost)/shop"), &gorm.Config{})
// ❌ 危险:Raw 字符串拼接
username := c.Query("username")
var users []User
db.Raw("SELECT * FROM users WHERE username = '" + username + "'").Scan(&users)
// ❌ 危险:Sprintf 拼接
db.Raw(fmt.Sprintf("SELECT * FROM users WHERE username = '%s'", username)).Scan(&users)
// ✅ 安全:参数占位符
db.Raw("SELECT * FROM users WHERE username = ?", username).Scan(&users)
// ✅ 安全:对象 API
db.Where("username = ?", username).Find(&users)
4.2 Where 的灵活条件
// ✅ 安全:GORM 的 Where 参数绑定
db.Where("username = ? AND age > ?", username, 18).Find(&users)
// ❌ 危险:Where 里用 fmt.Sprintf
db.Where(fmt.Sprintf("username = '%s'", username)).Find(&users)
// ❌ 危险:子句中拼接
db.Where("name LIKE ?", "%"+keyword+"%").Find(&products)
// 上面这个是安全的!参数绑定会处理 LIKE 中的 % 和 _
// ✅ 安全:虽然关键字里有特殊字符,但参数绑定保护了
4.3 GORM 的 Table 和 Model 方法
// ❌ 危险:Table 传入动态字符串
tableName := c.Query("table")
db.Table(tableName).Find(&results)
// ✅ 安全:白名单
ALLOWED_TABLES := map[string]interface{}{
"users": &[]User{},
"products": &[]Product{},
}
if model, ok := ALLOWED_TABLES[tableName]; ok {
db.Find(&model)
}
4.4 GORM Scope 中的风险
// ❌ 危险:Scope 里用 Raw
func adminScope(db *gorm.DB) *gorm.DB {
return db.Where(fmt.Sprintf("role = '%s'", currentRole))
}
// ✅ 安全:Scope 里也用参数绑定
func adminScope(db *gorm.DB) *gorm.DB {
return db.Where("role = ?", currentRole)
}
五、Django ORM 注入
5.1 extra() 和 raw()
from django.db import connection
from myapp.models import User
# ❌ 危险:raw() 拼接
username = request.GET['username']
User.objects.raw(f"SELECT * FROM users WHERE username = '{username}'")
# ❌ 危险:extra() 拼接
User.objects.extra(where=[f"username = '{username}'"])
# ✅ 安全:raw() 用参数
User.objects.raw("SELECT * FROM users WHERE username = %s", [username])
# ✅ 安全:extra() 用参数
User.objects.extra(where=["username = %s"], params=[username])
# ✅ 最安全:Django ORM 对象 API
User.objects.filter(username=username)
5.2 F() 和 Q() 对象
# F() 本身是安全的——它引用数据库列
from django.db.models import F
# ✅ 安全:F() 只是引用列名
Product.objects.update(price=F('price') * 1.1)
# ❌ 危险:F() 传入字符串表达式
Product.objects.filter(name=F("substr(name, 1, 1) = 'A'"))
# 某些版本 Django 中 F() 里的字符串可能被当作表达式处理
5.3 RawSQL
from django.db.models.expressions import RawSQL
# ❌ 危险:字符串拼接
qs = User.objects.annotate(
fake=RawSQL(f"SELECT '{request.GET['fake']}'", [])
)
# ✅ 安全:参数化
qs = User.objects.annotate(
fake=RawSQL("SELECT %s", [some_value])
)
六、ORM 注入的根本原因
6.1 "安全 API" vs "逃生口"
所有 ORM 都提供了"逃生口"——当你需要做复杂查询时,可以用原始 SQL。问题是这些逃生口往往被滥用。
安全 API(对象操作) 逃生口(原始 SQL)
┌────────────────┐ ┌────────────────┐
│ User.query │ │ db.raw("...") │
│ .filter(...) │ ──▶ │ engine.execute │
│ .where(...) │ │ Model.extra() │
│ .find() │ │ literal_column │
└────────────────┘ └────────────────┘
安全 危险!
6.2 开发者的"合理"理由
开发者使用逃生口通常有"合理"理由:
- "这个查询太复杂了,ORM 表达不了"
- "性能要求高,需要手写 SQL"
- "老项目遗留代码,重构成本高"
- "动态表名/列名,ORM 不支持参数化这些位置"
但这些理由都有安全替代方案:
- 复杂查询:用 CTE、子查询、多个 ORM 查询组合
- 性能:用 prepared statement + 优化器提示
- 动态表名/列名:白名单验证 + 标识符引用(
"table_name")
七、CodeQL 检测规则
为了在 CI/CD 中自动发现 ORM 注入,你可以配置 CodeQL 规则。
7.1 SQLAlchemy 注入检测
import python
from FunctionCall fc, Name n
where fc.getCallee().getName().matches("execute|query") and
fc.getArgument(0) instanceof StringLiteral and
exists(ReturnStmt ret |
ret.getFunction().getName().matches("text|raw|literal|column") and
ret.getReturnExpr() instanceof StringLiteral)
select fc, "Potential ORM injection"
7.2 GORM 注入检测
import go
from Call c, MemberAccess ma
where c.getCallee() = ma and
ma.getMemberName().matches("Raw|Exec") and
c.getArgument(0) instanceof BinaryExpr and
c.getArgument(0).getOperator() = "+"
select c, "GORM string concatenation in Raw/Exec"
八、ORM 注入防御清单
| 框架 | 禁止 | 允许 |
|---|---|---|
| SQLAlchemy | engine.execute(text(sql)) 中拼接字符串 |
text("... :param ...", {param: val}) |
| Sequelize | sequelize.query(\...${var}...`)` |
sequelize.query("... ? ...", {replacements: [var]}) |
| GORM | db.Raw("..."+var) / fmt.Sprintf(...) |
db.Raw("... ?", var) |
| Django | Model.objects.raw("..."+var) |
Model.objects.raw("... %s", [var]) |
8.1 项目级防护
- CI/CD 中加入静态扫描:SonarQube/Semgrep/CodeQL
- 代码规范审查:团队约定只用对象 API,原始 SQL 必须 code review
- 封装危险 API:在内部封装一层,自动转义/参数化
- 禁用危险 import:ESLint/Ruff 规则禁用某些函数
8.2 代码示例:安全封装
# Python:封装安全查询函数
def safe_query(conn, sql_template, **kwargs):
"""
安全的查询封装
sql_template: "SELECT * FROM users WHERE username = :username"
kwargs: {"username": username}
"""
return conn.execute(text(sql_template), kwargs)
# 使用
result = safe_query(engine, "SELECT * FROM users WHERE username = :user",
user=username)
ORM 不是银弹。它帮你做了很多安全的事,但它留的逃生口必须正确使用。防御 ORM 注入的核心是:在逃生口里也必须用参数绑定,不能因为"ORM 应该是安全的"就掉以轻心。