不要用 find_by_sql 接收用户输入拼接的 SQL 字符串,因其不支持参数化且不校验结构;安全做法是 SQL 全静态或变量经 sanitize_sql 处理且结构部分严格白名单校验。

直接结论:不要用 find_by_sql 接收任何用户输入拼接的 SQL 字符串——它不提供自动参数化,也不校验结构,裸用等于主动开后门。
为什么 find_by_sql 不做参数化防护?
find_by_sql 的设计目标是执行“已构造完成”的原始 SQL,它把整个字符串当作静态模板处理,连 ? 占位符都不识别(不像 where 或 connection.execute 支持)。传入 "SELECT * FROM users WHERE name = '#{params[:name]}'",日志里就原样打印出带用户输入的语句,毫无转义痕迹。
常见错误现象:
- 开发时没报错,测试也通过,但攻击者传
params[:name] = "admin' OR '1'='1"就能绕过条件查全表 - 用
ActiveRecord::Base.sanitize_sql包了一层,但只处理了值,字段名、函数名、子查询结构仍被直接拼入
必须白名单校验的 SQL 结构部分
字段名、表名、排序方向、GROUP BY 列、HAVING 条件中的函数名……这些都属于 SQL “结构”,无法用参数化保护。哪怕你把值全换成 ?,结构本身仍是注入入口。
实操建议:
- 动态列名(如搜索字段)必须硬编码白名单:
%w{email name status}.include?(params[:search_by]) ? params[:search_by] : 'email' - 排序方向只允许
'asc'或'desc':params[:order_dir]&.downcase.in?(['asc', 'desc']) ? params[:order_dir] : 'asc' - 避免从
params直接取表名或函数名;真有需求,用映射哈希转换:{'user_count' => 'COUNT(users.id)'}
如果非要用 find_by_sql,怎么写才安全?
唯一安全路径:SQL 字符串 100% 静态,或所有变量都经 ActiveRecord::Base.sanitize_sql 处理且结构部分已白名单过滤。
示例(安全):
sql = ActiveRecord::Base.sanitize_sql([
"SELECT users.*, COUNT(orders.id) AS order_count
FROM users
LEFT JOIN orders ON orders.user_id = users.id
WHERE users.status = ? AND users.created_at > ?
GROUP BY users.id
ORDER BY #{sort_column} #{sort_direction}",
params[:status],
params[:since]
])
@users = User.find_by_sql(sql)
注意点:
-
sort_column和sort_direction必须来自上一步白名单校验结果,不能是原始params -
sanitize_sql只保护数组第二个及之后的参数(即值),不碰字符串里的任何内容 - 别试图用
connection.quote手动包裹字段名——它只对值有效,且易漏转义
更推荐的替代方案
95% 的场景下,find_by_sql 都不是必须的。优先走 ActiveRecord 原生链式方法:
- 复杂 JOIN + 聚合?用
joins+group+select:User.joins(:orders).group('users.id').select('users.*, COUNT(orders.id)') - JSONB 查询?Rails 7 原生支持:
where("data @> ?", {role: "admin"}.to_json) - 窗口函数?用
select("ROW_NUMBER() OVER (ORDER BY created_at)"),配合白名单字段名
真正容易被忽略的是:一旦你决定手写 SQL,你就同时承担了结构校验 + 值消毒 + 类型约束三重责任——而框架只帮你做了最后一项。

















