active record 在 rails 7 中默认防 sql 注入,但仅限参数化查询;字符串插值(如 "name = '#{params[:name]}'")会绕过所有防护,必须使用问号占位符、哈希或命名绑定,动态字段(如 order)需白名单校验。

Active Record 在 Rails 7 中默认就防 SQL 注入——但只对参数化查询部分生效。你不需要额外配置,也不该去“开启”某个开关;真正决定是否安全的,是你写查询时有没有绕过它的绑定机制。
为什么 User.where("name = '#{params[:name]}'") 依然会中招
这种写法直接把用户输入插进字符串,Active Record 完全不介入、不转义、不拦截。日志里能看到原始 SQL 被拼进去,且值没加引号或转义,说明已跳过所有防护层。
常见错误现象包括:
- 查询返回意外多条记录(比如
params[:name] = "admin' OR '1'='1") - 数据库报错如
PG::SyntaxError或SQLite3::SQLException,提示 SQL 结构异常 -
User.find_by("email = '#{params[:email]}'")在开发日志中显示未加引号的 raw value
根本原因:字符串插值绕过了 Active Record 的参数绑定,交由数据库驱动直译——MySQL、PostgreSQL、SQLite 全部等效裸奔。
where 和 find_by 必须用参数化写法
两者语义不同,但防注入逻辑一致:必须走问号占位符或哈希传参。
-
User.where("email = ? AND active = ?", params[:email], true)—— 顺序绑定,最通用 -
User.where(email: params[:email], active: true)—— 哈希写法,自动处理字段名和值,推荐用于简单等值条件 -
User.find_by("email = :email", email: params[:email])—— 命名绑定可行,但不如哈希直观,少用 -
User.find_by("email = '#{params[:email]}'")—— ❌ 禁止。哪怕加了strip、presence或正则校验也拦不住注入
注意:find_by 返回单个实例或 nil,where 返回关系对象(可链式调用),选哪个取决于业务逻辑,不是安全差异点。
order/group/having 这类动态字段必须白名单校验
order 不接受 ? 占位符,所以 User.order("#{params[:sort]} #{params[:dir]}") 是高危操作——攻击者传 params[:sort] = "email; DROP TABLE users; --" 就能执行任意 SQL。
- 字段名必须来自白名单:
%w{email name created_at}.include?(params[:sort]) ? params[:sort] : "created_at" - 排序方向也需校验:
%w{asc desc}.include?(params[:dir]) ? params[:dir] : "desc" - 更健壮的做法是用
User.column_names动态生成白名单,避免硬编码遗漏 - 不要依赖
ActiveRecord::Base.sanitize_sql处理字段名——它只处理值,不处理结构
容易被忽略的一点:白名单必须在进入 order 前完成,不能靠数据库层或中间件补救。
raw SQL 查询(如 find_by_sql)必须手动 sanitize
一旦调用 User.find_by_sql("SELECT * FROM users WHERE name = '#{params[:name]}'"),Active Record 的防护完全失效。
- 唯一安全方式是配合
ActiveRecord::Base.sanitize_sql_for_conditions或connection.quote手动转义值(仅限值,不保字段) - 例如:
User.find_by_sql(["SELECT * FROM users WHERE name = ?", params[:name]])—— 这种仍走参数化,可用 - 但
User.find_by_sql("SELECT * FROM users ORDER BY #{params[:sort]}")必须先白名单校验params[:sort] - Rails 7 中
sanitize_sql已标记为私有方法,不建议直接调用;优先重构为 ActiveRecord 链式调用
复杂点在于:很多老项目把搜索逻辑塞进 find_by_sql,改起来要动业务耦合点,这时白名单 + 字段名映射表(如 {'user_email' => 'email'})比硬编码更可持续。











