Skip to content

Commit d630773

Browse files
authored
Merge pull request #14627 from alexrford/rb/update_all_sink
Ruby: refine `ActiveRecord` `update_all` as an SQL sink
2 parents 78e0f69 + 8db23dc commit d630773

File tree

4 files changed

+121
-79
lines changed

4 files changed

+121
-79
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: minorAnalysis
3+
---
4+
* Improved modeling for `ActiveRecord`s `update_all` method

ruby/ql/lib/codeql/ruby/frameworks/ActiveRecord.qll

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ private predicate sqlFragmentArgumentInner(DataFlow::CallNode call, DataFlow::No
173173
"delete_all", "delete_by", "destroy_all", "destroy_by", "exists?", "find_by", "find_by!",
174174
"find_or_create_by", "find_or_create_by!", "find_or_initialize_by", "find_by_sql", "from",
175175
"group", "having", "joins", "lock", "not", "order", "reorder", "pluck", "where", "rewhere",
176-
"select", "reselect", "update_all"
176+
"select", "reselect"
177177
]) and
178178
sink = call.getArgument(0)
179179
or
@@ -198,6 +198,20 @@ private predicate sqlFragmentArgumentInner(DataFlow::CallNode call, DataFlow::No
198198
or
199199
call = activeRecordConnectionInstance().getAMethodCall("execute") and
200200
sink = call.getArgument(0)
201+
or
202+
call = activeRecordQueryBuilderCall("update_all") and
203+
(
204+
// `update_all([sink, var1, var2, var3])`
205+
sink = call.getArgument(0).getALocalSource().(DataFlow::ArrayLiteralNode).getElement(0)
206+
or
207+
// or arg0 is not of a known "safe" type
208+
sink = call.getArgument(0) and
209+
not (
210+
sink.getALocalSource() = any(DataFlow::ArrayLiteralNode arr) or
211+
sink.getALocalSource() = any(DataFlow::HashLiteralNode hash) or
212+
sink.getALocalSource() = any(DataFlow::PairNode pair)
213+
)
214+
)
201215
}
202216

203217
private predicate sqlFragmentArgument(DataFlow::CallNode call, DataFlow::Node sink) {

ruby/ql/test/query-tests/security/cwe-089/ActiveRecordInjection.rb

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,21 @@ def some_request_handler
9090
# BAD: executes `UPDATE "users" SET #{params[:fields]}`
9191
# where `params[:fields]` is unsanitized
9292
User.update_all(params[:fields])
93-
93+
94+
# GOOD -- `update_all` sanitizes its bind variable arguments
95+
User.find_by(name: params[:user_name])
96+
.update_all(['name = ?', params[:new_user_name]])
97+
98+
# BAD -- `update_all` does not sanitize its query (array arg)
99+
User.find_by(name: params[:user_name])
100+
.update_all(["name = '#{params[:new_user_name]}'"])
101+
102+
# BAD -- `update_all` does not sanitize its query (string arg)
103+
User.find_by(name: params[:user_name])
104+
.update_all("name = '#{params[:new_user_name]}'")
105+
94106
User.reorder(params[:direction])
95-
107+
96108
User.count_by_sql(params[:custom_sql_query])
97109
end
98110
end
@@ -168,13 +180,13 @@ def index
168180
result = Regression.find_by_sql(query)
169181
end
170182

171-
183+
172184
def permitted_params
173185
params.require(:my_key).permit(:id, :user_id, :my_type)
174186
end
175-
187+
176188
def show
177189
ActiveRecord::Base.connection.execute("SELECT * FROM users WHERE id = #{permitted_params[:user_id]}")
178190
Regression.connection.execute("SELECT * FROM users WHERE id = #{permitted_params[:user_id]}")
179191
end
180-
end
192+
end

0 commit comments

Comments
 (0)