Active Record 默认防 SQL 注入,但仅限参数化查询;User.where("name = '#{params[:name]}'") 高危,因直接拼接用户输入,绕过绑定层,使攻击者可注入恶意 SQL 如 "admin' --" 或 "; DROP TABLE users;"。

Active Record 默认就能防 SQL 注入,但前提是别绕过它——所有用户输入必须走参数化查询,不能字符串拼接。
为什么 User.where("name = '#{params[:name]}'") 是高危写法
这种写法会把用户输入直接塞进 SQL 字符串里,攻击者传 params[:name] = "admin' -- " 就能注释掉后续条件,甚至追加 ; DROP TABLE users;。Active Record 完全不介入、不转义、不拦截,相当于裸奔。
- 错误现象:
User.find_by("email = '#{params[:email]}'")在日志里看到原始 SQL 被拼进去,且没带引号包裹或转义 - 根本原因:绕过了 Active Record 的参数绑定层,交由数据库驱动直译字符串
- 兼容性影响:MySQL/PostgreSQL/SQLite 全部中招,跟数据库类型无关
User.where 和 User.find_by 的安全用法差异
两者都支持参数化,但语义和容错不同:where 返回关系对象(可链式调用),find_by 直接返回单个实例或 nil。关键都在怎么传条件。
- ✅ 推荐:
User.where("email = ? AND active = ?", params[:email], true)—— 问号占位,顺序绑定 - ✅ 推荐:
User.where(email: params[:email], active: true)—— 哈希写法,Active Record 自动处理键名和值 - ⚠️ 谨慎:
User.find_by("email = :email", email: params[:email])—— 命名绑定可行,但不如哈希直观 - ❌ 禁止:
User.find_by("email = '#{params[:email]}'")—— 单引号+插值,无任何防护
遇到 order、group 这类动态字段时怎么防注入
order 不接受参数化占位符,所以不能像 where 那样直接传变量。必须白名单校验字段名和方向,否则攻击者可以传 params[:sort] = "email; DROP TABLE users; --"。
- 正确做法:先限定可用字段,再拼接(不是用户输入直接拼)
allowed_sorts = %w[email name created_at]<br>sort_column = allowed_sorts.include?(params[:sort]) ? params[:sort] : "created_at"<br>direction = %w[asc desc].include?(params[:direction]) ? params[:direction] : "desc"<br>User.order("#{sort_column} #{direction}") - 替代方案:用
Arel构建安全表达式,但多数场景白名单更轻量可靠 - 容易踩的坑:以为
params[:sort].presence || "created_at"就够了——没校验内容合法性,照样被注入
什么时候必须用原生 SQL?如何守住底线
当 Active Record 生成的 SQL 效率太低,或需要窗口函数、CTE、复杂 UNION 时,find_by_sql 或 connection.execute 才是合理选择。但它们不自动防注入,你得自己负责。
- ✅ 可控场景:
User.find_by_sql("SELECT * FROM users WHERE status = 'active' ORDER BY created_at DESC")—— 字符串全静态,无用户输入 - ✅ 带参数:用
ActiveRecord::Base.connection.select_all+ 手动绑定(需查 DB adapter 文档),或封装到模型方法里做白名单过滤 - ❌ 危险操作:
ActiveRecord::Base.connection.execute("UPDATE users SET name = '#{params[:name]}' WHERE id = #{params[:id]}")—— 两个变量都没防护 - 性能提示:原生 SQL 虽快,但失去 Active Record 的缓存、关联预加载等能力,别为一点性能放弃安全护栏
最常被忽略的点是 order 和 group —— 它们不走参数绑定流程,开发者容易误以为“用了 Active Record 就万事大吉”,结果在排序字段上开了后门。白名单不是过度设计,是唯一靠谱的解法。

















