一、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 项目级防护

  1. CI/CD 中加入静态扫描:SonarQube/Semgrep/CodeQL
  2. 代码规范审查:团队约定只用对象 API,原始 SQL 必须 code review
  3. 封装危险 API:在内部封装一层,自动转义/参数化
  4. 禁用危险 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 应该是安全的"就掉以轻心。