直接拼接 raw sql 字符串必然导致注入,因 activerecord 不检查 find_by_sql、connection.execute 等传入的字符串,ruby 插值在参数化前已执行;正确做法仅用问号占位符、哈希式或命名占位符三种参数化形式。

直接拼接 raw SQL 字符串必然导致注入
Active Record 不会检查你传给 find_by_sql、connection.execute 或 select_all 的字符串内容——它原样交给数据库驱动执行。只要里面出现 "#{params[:input]}",就等于把 SQL 解析权完全交给了用户。
常见错误现象包括:
- 日志中看到未加引号的原始值出现在 SQL 里,比如
WHERE name = admin' OR '1'='1 - 数据库报错如
PG::SyntaxError或SQLite3::SQLException,提示语法异常 - 查询返回远超预期的数据,或意外修改/删除记录
根本原因:Ruby 字符串插值发生在 ActiveRecord 介入之前,参数化机制根本没机会启动。
where 和 find_by 中混用字符串插值是隐形炸弹
很多人以为只要用了 where 就安全,但写成 User.where("name = '#{params[:name]}'") 或 User.find_by("email = '#{params[:email]}'"),就彻底绕过了防护。这种写法在开发日志里会显示 raw value,没有任何转义痕迹。
正确做法只保留三种参数化形式:
-
User.where("status = ? AND created_at > ?", "active", params[:since])—— 问号占位符,顺序绑定 -
User.where(email: params[:email], active: true)—— 哈希式,字段名和值都受控 -
User.where("email = :email", email: params[:email])—— 命名占位符,可读性稍好但不推荐用于复杂条件
注意:find_by 和 where 在防注入上无差异,区别仅在于返回值类型(单实例 vs Relation),别因语义选错写法而误判安全性。
order/group/having 等动态子句必须白名单校验
order 不接受问号占位符,所以 User.order("#{params[:sort]} #{params[:direction]}") 是高危操作。攻击者传 params[:sort] = "email; DROP TABLE users; --" 就能执行任意语句。
字段名和方向都得校验:
- 字段白名单优先用
User.column_names动态生成,避免硬编码过期 - 排序方向只允许
%w{asc desc},其他一律 fallback 到默认值 - 不要试图用正则或
strip过滤——单引号、分号、注释符都拦不住
示例:
User.order("#{%w{email name created_at}.include?(params[:sort]) ? params[:sort] : 'created_at'} #{%w{asc desc}.include?(params[:direction]) ? params[:direction] : 'desc'})
Ransack 搜索必须关闭 ignore_unknown_conditions
Ransack 默认忽略未知条件,这会让攻击者通过构造非法谓词(如 password_eq)绕过模型层校验。生产环境必须显式禁用:
Ransack.configure do |config| config.ignore_unknown_conditions = false end
同时确保搜索参数走强参数白名单:
-
params.permit(q: [:name_cont, :email_eq])明确限定可用字段和谓词 - 避免
params[:q].to_unsafe_h或手动拼接条件字符串 - 自定义
ransacker时,内部仍要用参数化查询,不能拼 raw SQL
真正危险的从来不是 ActiveRecord 本身,而是你主动跳过它去拼字符串——哪怕只有一处,整个查询链就失效了。











