必须禁用字符串插值,因find_by_sql和connection.execute绕过activerecord参数化机制;正确做法仅限问号占位符、命名占位符及白名单校验动态字段名与排序方向。

find_by_sql 和 connection.execute 必须禁用字符串插值
这两个方法完全绕过 ActiveRecord 的参数化机制,传入的 SQL 字符串会被原样交给数据库驱动执行。Ruby 层的字符串插值("#{params[:name]}")发生在参数绑定之前,没有任何转义机会。
-
User.find_by_sql("SELECT * FROM users WHERE name = '#{params[:name]}'")—— ❌ 直接裸奔,哪怕加了strip或正则也无效 -
ActiveRecord::Base.connection.execute("UPDATE users SET status = '#{params[:status]}'")—— ❌ 同样危险,且可能触发多语句执行(如 MySQL 中的;分隔) - 正确写法只保留三种安全形式:
find_by_sql("SELECT * FROM users WHERE name = ?", params[:name])、find_by_sql("SELECT * FROM users WHERE name = :name", name: params[:name])、或用connection.exec_query配合问号占位符
动态字段名(如 order/group/having)必须白名单硬编码
order 不接受 ? 占位符,Active Record 也不会校验字段名是否合法——这事必须你来守门。用户控制排序方向或分组字段时,若直接拼接,params[:sort] = "email; DROP TABLE users; --" 就能生效。
- 方向白名单:
%w{asc desc}.include?(params[:dir]) ? params[:dir] : "desc" - 字段白名单更推荐用
User.column_names动态生成,避免硬编码遗漏新字段;但注意:该数组来自 schema,不能依赖 DB 运行时状态(如临时表) - 不要用正则过滤字段名(如
/\A[a-z_]+\z/),它拦不住email ASC, (SELECT password FROM admins)这类嵌套注入
强参数没配好,会倒逼你写危险查询
强参数(params.permit)本身不防 SQL 注入,但它决定你能不能放心地把 params 直接喂给 where 或 find_by。一旦白名单漏配,你就可能 fallback 到手拼 SQL。
本文档主要讲述的是Ruby on Rails字符串处理;在Ruby中创建一个字符串有多种方式。可以有两种方式表示一个字符串:用一对单引号包围字符('str')或用一对双引号包围字符("str") 这两种形式的区别在于对于包围的字符串的处理,用双引号构造的字符串能处理更多的转移字符。 希望本文档会给有需要的朋友带来帮助;感兴趣的朋友可以过来看看
- 比如
params[:q]没进 permit,你就可能写User.where("name LIKE '%#{params[:q]}%'")—— 这里params[:q]已经是未过滤的原始输入 -
User.where(user_params)安全的前提是user_params明确声明了允许字段,否则params.to_unsafe_h或手动取值极易引入漏洞 - Ransack 类搜索也依赖强参数:如
params.permit(:q => [:name_eq, :email_cont]),否则q[password_eq]可能绕过校验
raw execute 场景下,别信“转义函数”
有人试图用 ActiveRecord::Base.sanitize_sql 或自定义转义逻辑处理用户输入再拼进 raw SQL,这是错觉。这些函数不是为运行时拼接设计的,且无法覆盖所有 DB 特性(如 PostgreSQL 的 array_agg、MySQL 的变量赋值)。
-
connection.execute(sanitize_sql(["SELECT * FROM users WHERE name = ?", params[:name]]))—— ✅ 安全,因为sanitize_sql在这里只是辅助构造参数化语句 -
connection.execute("SELECT * FROM users WHERE name = '#{sanitize(params[:name])}'")—— ❌ 危险,sanitize函数无法替代预编译,且不同 DB 转义规则不一致 - 真正复杂场景(如动态表名)只能靠字典映射:
table_map = {"orders_2024" => "orders_q1"},用户传非法键就报 400,绝不 fallback
最麻烦的点不在语法,而在权责边界:Active Record 的防护只覆盖它自己构建的查询路径;一旦你调用 find_by_sql 或 connection.execute,你就退回到手写 SQL 的风险模型——此时没有框架兜底,白名单和参数化都得你亲手写死。










