Skip to content

Commit 5923eb4

Browse files
authored
Merge pull request #9566 from alexrford/ruby/activerecord-findby-dynamic
Ruby: recognize ActiveRecord `find_by_x` methods
2 parents cac53b5 + c44a686 commit 5923eb4

File tree

4 files changed

+101
-118
lines changed

4 files changed

+101
-118
lines changed

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ abstract class ActiveRecordModelInstantiation extends OrmInstantiation::Range,
240240
// Names of class methods on ActiveRecord models that may return one or more
241241
// instances of that model. This also includes the `initialize` method.
242242
// See https://api.rubyonrails.org/classes/ActiveRecord/FinderMethods.html
243-
private string finderMethodName() {
243+
private string staticFinderMethodName() {
244244
exists(string baseName |
245245
baseName =
246246
[
@@ -287,7 +287,12 @@ private class ActiveRecordModelFinderCall extends ActiveRecordModelInstantiation
287287
callScope = cls.getAMethod()
288288
)
289289
) and
290-
call.getMethodName() = finderMethodName()
290+
(
291+
call.getMethodName() = staticFinderMethodName()
292+
or
293+
// dynamically generated finder methods
294+
call.getMethodName().indexOf("find_by_") = 0
295+
)
291296
)
292297
}
293298

ruby/ql/test/library-tests/frameworks/ActionController.expected

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
actionControllerControllerClasses
2-
| ActiveRecordInjection.rb:27:1:58:3 | FooController |
3-
| ActiveRecordInjection.rb:60:1:90:3 | BarController |
4-
| ActiveRecordInjection.rb:92:1:96:3 | BazController |
2+
| ActiveRecord.rb:23:1:39:3 | FooController |
3+
| ActiveRecord.rb:41:1:64:3 | BarController |
4+
| ActiveRecord.rb:66:1:70:3 | BazController |
55
| app/controllers/comments_controller.rb:1:1:7:3 | CommentsController |
66
| app/controllers/foo/bars_controller.rb:3:1:31:3 | BarsController |
77
| app/controllers/photos_controller.rb:1:1:4:3 | PhotosController |
88
| app/controllers/posts_controller.rb:1:1:10:3 | PostsController |
99
| app/controllers/users/notifications_controller.rb:2:3:5:5 | NotificationsController |
1010
actionControllerActionMethods
11-
| ActiveRecordInjection.rb:32:3:57:5 | some_request_handler |
12-
| ActiveRecordInjection.rb:61:3:69:5 | some_other_request_handler |
13-
| ActiveRecordInjection.rb:71:3:89:5 | safe_paths |
14-
| ActiveRecordInjection.rb:93:3:95:5 | yet_another_handler |
11+
| ActiveRecord.rb:27:3:38:5 | some_request_handler |
12+
| ActiveRecord.rb:42:3:47:5 | some_other_request_handler |
13+
| ActiveRecord.rb:49:3:63:5 | safe_paths |
14+
| ActiveRecord.rb:67:3:69:5 | yet_another_handler |
1515
| app/controllers/comments_controller.rb:2:3:3:5 | index |
1616
| app/controllers/comments_controller.rb:5:3:6:5 | show |
1717
| app/controllers/foo/bars_controller.rb:5:3:7:5 | index |
@@ -23,38 +23,38 @@ actionControllerActionMethods
2323
| app/controllers/posts_controller.rb:8:3:9:5 | upvote |
2424
| app/controllers/users/notifications_controller.rb:3:5:4:7 | mark_as_read |
2525
paramsCalls
26-
| ActiveRecordInjection.rb:35:30:35:35 | call to params |
27-
| ActiveRecordInjection.rb:39:29:39:34 | call to params |
28-
| ActiveRecordInjection.rb:43:31:43:36 | call to params |
29-
| ActiveRecordInjection.rb:48:21:48:26 | call to params |
30-
| ActiveRecordInjection.rb:54:34:54:39 | call to params |
31-
| ActiveRecordInjection.rb:56:23:56:28 | call to params |
32-
| ActiveRecordInjection.rb:56:38:56:43 | call to params |
33-
| ActiveRecordInjection.rb:62:10:62:15 | call to params |
34-
| ActiveRecordInjection.rb:72:11:72:16 | call to params |
35-
| ActiveRecordInjection.rb:77:12:77:17 | call to params |
36-
| ActiveRecordInjection.rb:83:12:83:17 | call to params |
37-
| ActiveRecordInjection.rb:88:15:88:20 | call to params |
38-
| ActiveRecordInjection.rb:94:21:94:26 | call to params |
26+
| ActiveRecord.rb:28:30:28:35 | call to params |
27+
| ActiveRecord.rb:29:29:29:34 | call to params |
28+
| ActiveRecord.rb:30:31:30:36 | call to params |
29+
| ActiveRecord.rb:32:21:32:26 | call to params |
30+
| ActiveRecord.rb:34:34:34:39 | call to params |
31+
| ActiveRecord.rb:35:23:35:28 | call to params |
32+
| ActiveRecord.rb:35:38:35:43 | call to params |
33+
| ActiveRecord.rb:43:10:43:15 | call to params |
34+
| ActiveRecord.rb:50:11:50:16 | call to params |
35+
| ActiveRecord.rb:54:12:54:17 | call to params |
36+
| ActiveRecord.rb:59:12:59:17 | call to params |
37+
| ActiveRecord.rb:62:15:62:20 | call to params |
38+
| ActiveRecord.rb:68:21:68:26 | call to params |
3939
| app/controllers/foo/bars_controller.rb:13:21:13:26 | call to params |
4040
| app/controllers/foo/bars_controller.rb:14:10:14:15 | call to params |
4141
| app/controllers/foo/bars_controller.rb:21:21:21:26 | call to params |
4242
| app/controllers/foo/bars_controller.rb:22:10:22:15 | call to params |
4343
| app/views/foo/bars/show.html.erb:5:9:5:14 | call to params |
4444
paramsSources
45-
| ActiveRecordInjection.rb:35:30:35:35 | call to params |
46-
| ActiveRecordInjection.rb:39:29:39:34 | call to params |
47-
| ActiveRecordInjection.rb:43:31:43:36 | call to params |
48-
| ActiveRecordInjection.rb:48:21:48:26 | call to params |
49-
| ActiveRecordInjection.rb:54:34:54:39 | call to params |
50-
| ActiveRecordInjection.rb:56:23:56:28 | call to params |
51-
| ActiveRecordInjection.rb:56:38:56:43 | call to params |
52-
| ActiveRecordInjection.rb:62:10:62:15 | call to params |
53-
| ActiveRecordInjection.rb:72:11:72:16 | call to params |
54-
| ActiveRecordInjection.rb:77:12:77:17 | call to params |
55-
| ActiveRecordInjection.rb:83:12:83:17 | call to params |
56-
| ActiveRecordInjection.rb:88:15:88:20 | call to params |
57-
| ActiveRecordInjection.rb:94:21:94:26 | call to params |
45+
| ActiveRecord.rb:28:30:28:35 | call to params |
46+
| ActiveRecord.rb:29:29:29:34 | call to params |
47+
| ActiveRecord.rb:30:31:30:36 | call to params |
48+
| ActiveRecord.rb:32:21:32:26 | call to params |
49+
| ActiveRecord.rb:34:34:34:39 | call to params |
50+
| ActiveRecord.rb:35:23:35:28 | call to params |
51+
| ActiveRecord.rb:35:38:35:43 | call to params |
52+
| ActiveRecord.rb:43:10:43:15 | call to params |
53+
| ActiveRecord.rb:50:11:50:16 | call to params |
54+
| ActiveRecord.rb:54:12:54:17 | call to params |
55+
| ActiveRecord.rb:59:12:59:17 | call to params |
56+
| ActiveRecord.rb:62:15:62:20 | call to params |
57+
| ActiveRecord.rb:68:21:68:26 | call to params |
5858
| app/controllers/foo/bars_controller.rb:13:21:13:26 | call to params |
5959
| app/controllers/foo/bars_controller.rb:14:10:14:15 | call to params |
6060
| app/controllers/foo/bars_controller.rb:21:21:21:26 | call to params |
Lines changed: 58 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,64 @@
11
activeRecordModelClasses
2-
| ActiveRecordInjection.rb:1:1:3:3 | UserGroup |
3-
| ActiveRecordInjection.rb:5:1:17:3 | User |
4-
| ActiveRecordInjection.rb:19:1:25:3 | Admin |
2+
| ActiveRecord.rb:1:1:3:3 | UserGroup |
3+
| ActiveRecord.rb:5:1:15:3 | User |
4+
| ActiveRecord.rb:17:1:21:3 | Admin |
55
activeRecordInstances
6-
| ActiveRecordInjection.rb:10:5:10:68 | call to find |
7-
| ActiveRecordInjection.rb:15:5:15:40 | call to find_by |
8-
| ActiveRecordInjection.rb:79:5:81:7 | if ... |
9-
| ActiveRecordInjection.rb:79:43:80:40 | then ... |
10-
| ActiveRecordInjection.rb:80:7:80:40 | call to find_by |
11-
| ActiveRecordInjection.rb:85:5:85:33 | call to find_by |
12-
| ActiveRecordInjection.rb:88:5:88:34 | call to find |
6+
| ActiveRecord.rb:9:5:9:68 | call to find |
7+
| ActiveRecord.rb:13:5:13:40 | call to find_by |
8+
| ActiveRecord.rb:36:5:36:30 | call to find_by_name |
9+
| ActiveRecord.rb:55:5:57:7 | if ... |
10+
| ActiveRecord.rb:55:43:56:40 | then ... |
11+
| ActiveRecord.rb:56:7:56:40 | call to find_by |
12+
| ActiveRecord.rb:60:5:60:33 | call to find_by |
13+
| ActiveRecord.rb:62:5:62:34 | call to find |
1314
activeRecordSqlExecutionRanges
14-
| ActiveRecordInjection.rb:10:33:10:67 | "name='#{...}' and pass='#{...}'" |
15-
| ActiveRecordInjection.rb:23:16:23:24 | condition |
16-
| ActiveRecordInjection.rb:35:30:35:44 | ...[...] |
17-
| ActiveRecordInjection.rb:39:20:39:42 | "id = '#{...}'" |
18-
| ActiveRecordInjection.rb:43:22:43:44 | "id = '#{...}'" |
19-
| ActiveRecordInjection.rb:47:16:47:21 | <<-SQL |
20-
| ActiveRecordInjection.rb:54:20:54:47 | "user.id = '#{...}'" |
21-
| ActiveRecordInjection.rb:68:20:68:32 | ... + ... |
22-
| ActiveRecordInjection.rb:75:16:75:28 | "name #{...}" |
23-
| ActiveRecordInjection.rb:80:20:80:39 | "username = #{...}" |
15+
| ActiveRecord.rb:9:33:9:67 | "name='#{...}' and pass='#{...}'" |
16+
| ActiveRecord.rb:19:16:19:24 | condition |
17+
| ActiveRecord.rb:28:30:28:44 | ...[...] |
18+
| ActiveRecord.rb:29:20:29:42 | "id = '#{...}'" |
19+
| ActiveRecord.rb:30:22:30:44 | "id = '#{...}'" |
20+
| ActiveRecord.rb:31:16:31:21 | <<-SQL |
21+
| ActiveRecord.rb:34:20:34:47 | "user.id = '#{...}'" |
22+
| ActiveRecord.rb:46:20:46:32 | ... + ... |
23+
| ActiveRecord.rb:52:16:52:28 | "name #{...}" |
24+
| ActiveRecord.rb:56:20:56:39 | "username = #{...}" |
2425
activeRecordModelClassMethodCalls
25-
| ActiveRecordInjection.rb:2:3:2:17 | call to has_many |
26-
| ActiveRecordInjection.rb:6:3:6:24 | call to belongs_to |
27-
| ActiveRecordInjection.rb:10:5:10:68 | call to find |
28-
| ActiveRecordInjection.rb:15:5:15:40 | call to find_by |
29-
| ActiveRecordInjection.rb:15:5:15:46 | call to users |
30-
| ActiveRecordInjection.rb:23:5:23:25 | call to destroy_by |
31-
| ActiveRecordInjection.rb:35:5:35:45 | call to calculate |
32-
| ActiveRecordInjection.rb:39:5:39:43 | call to delete_by |
33-
| ActiveRecordInjection.rb:43:5:43:46 | call to destroy_by |
34-
| ActiveRecordInjection.rb:47:5:47:35 | call to where |
35-
| ActiveRecordInjection.rb:54:5:54:14 | call to where |
36-
| ActiveRecordInjection.rb:54:5:54:48 | call to not |
37-
| ActiveRecordInjection.rb:56:5:56:51 | call to authenticate |
38-
| ActiveRecordInjection.rb:68:5:68:33 | call to delete_by |
39-
| ActiveRecordInjection.rb:75:5:75:29 | call to order |
40-
| ActiveRecordInjection.rb:80:7:80:40 | call to find_by |
41-
| ActiveRecordInjection.rb:85:5:85:33 | call to find_by |
42-
| ActiveRecordInjection.rb:88:5:88:34 | call to find |
43-
| ActiveRecordInjection.rb:94:5:94:45 | call to delete_by |
26+
| ActiveRecord.rb:2:3:2:17 | call to has_many |
27+
| ActiveRecord.rb:6:3:6:24 | call to belongs_to |
28+
| ActiveRecord.rb:9:5:9:68 | call to find |
29+
| ActiveRecord.rb:13:5:13:40 | call to find_by |
30+
| ActiveRecord.rb:13:5:13:46 | call to users |
31+
| ActiveRecord.rb:19:5:19:25 | call to destroy_by |
32+
| ActiveRecord.rb:28:5:28:45 | call to calculate |
33+
| ActiveRecord.rb:29:5:29:43 | call to delete_by |
34+
| ActiveRecord.rb:30:5:30:46 | call to destroy_by |
35+
| ActiveRecord.rb:31:5:31:35 | call to where |
36+
| ActiveRecord.rb:34:5:34:14 | call to where |
37+
| ActiveRecord.rb:34:5:34:48 | call to not |
38+
| ActiveRecord.rb:35:5:35:51 | call to authenticate |
39+
| ActiveRecord.rb:36:5:36:30 | call to find_by_name |
40+
| ActiveRecord.rb:37:5:37:36 | call to not_a_find_by_method |
41+
| ActiveRecord.rb:46:5:46:33 | call to delete_by |
42+
| ActiveRecord.rb:52:5:52:29 | call to order |
43+
| ActiveRecord.rb:56:7:56:40 | call to find_by |
44+
| ActiveRecord.rb:60:5:60:33 | call to find_by |
45+
| ActiveRecord.rb:62:5:62:34 | call to find |
46+
| ActiveRecord.rb:68:5:68:45 | call to delete_by |
4447
potentiallyUnsafeSqlExecutingMethodCall
45-
| ActiveRecordInjection.rb:10:5:10:68 | call to find |
46-
| ActiveRecordInjection.rb:23:5:23:25 | call to destroy_by |
47-
| ActiveRecordInjection.rb:35:5:35:45 | call to calculate |
48-
| ActiveRecordInjection.rb:39:5:39:43 | call to delete_by |
49-
| ActiveRecordInjection.rb:43:5:43:46 | call to destroy_by |
50-
| ActiveRecordInjection.rb:47:5:47:35 | call to where |
51-
| ActiveRecordInjection.rb:54:5:54:48 | call to not |
52-
| ActiveRecordInjection.rb:68:5:68:33 | call to delete_by |
53-
| ActiveRecordInjection.rb:75:5:75:29 | call to order |
54-
| ActiveRecordInjection.rb:80:7:80:40 | call to find_by |
48+
| ActiveRecord.rb:9:5:9:68 | call to find |
49+
| ActiveRecord.rb:19:5:19:25 | call to destroy_by |
50+
| ActiveRecord.rb:28:5:28:45 | call to calculate |
51+
| ActiveRecord.rb:29:5:29:43 | call to delete_by |
52+
| ActiveRecord.rb:30:5:30:46 | call to destroy_by |
53+
| ActiveRecord.rb:31:5:31:35 | call to where |
54+
| ActiveRecord.rb:34:5:34:48 | call to not |
55+
| ActiveRecord.rb:46:5:46:33 | call to delete_by |
56+
| ActiveRecord.rb:52:5:52:29 | call to order |
57+
| ActiveRecord.rb:56:7:56:40 | call to find_by |
5558
activeRecordModelInstantiations
56-
| ActiveRecordInjection.rb:10:5:10:68 | call to find | ActiveRecordInjection.rb:5:1:17:3 | User |
57-
| ActiveRecordInjection.rb:15:5:15:40 | call to find_by | ActiveRecordInjection.rb:1:1:3:3 | UserGroup |
58-
| ActiveRecordInjection.rb:80:7:80:40 | call to find_by | ActiveRecordInjection.rb:5:1:17:3 | User |
59-
| ActiveRecordInjection.rb:85:5:85:33 | call to find_by | ActiveRecordInjection.rb:5:1:17:3 | User |
60-
| ActiveRecordInjection.rb:88:5:88:34 | call to find | ActiveRecordInjection.rb:5:1:17:3 | User |
59+
| ActiveRecord.rb:9:5:9:68 | call to find | ActiveRecord.rb:5:1:15:3 | User |
60+
| ActiveRecord.rb:13:5:13:40 | call to find_by | ActiveRecord.rb:1:1:3:3 | UserGroup |
61+
| ActiveRecord.rb:36:5:36:30 | call to find_by_name | ActiveRecord.rb:5:1:15:3 | User |
62+
| ActiveRecord.rb:56:7:56:40 | call to find_by | ActiveRecord.rb:5:1:15:3 | User |
63+
| ActiveRecord.rb:60:5:60:33 | call to find_by | ActiveRecord.rb:5:1:15:3 | User |
64+
| ActiveRecord.rb:62:5:62:34 | call to find | ActiveRecord.rb:5:1:15:3 | User |

ruby/ql/test/library-tests/frameworks/ActiveRecordInjection.rb renamed to ruby/ql/test/library-tests/frameworks/ActiveRecord.rb

Lines changed: 3 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,16 @@ class User < ApplicationRecord
66
belongs_to :user_group
77

88
def self.authenticate(name, pass)
9-
# BAD: possible untrusted input interpolated into SQL fragment
109
find(:first, :conditions => "name='#{name}' and pass='#{pass}'")
1110
end
1211

1312
def self.from(user_group_id)
14-
# GOOD: `find_by` with hash argument
1513
UserGroup.find_by(id: user_group_id).users
1614
end
1715
end
1816

1917
class Admin < User
2018
def self.delete_by(condition = nil)
21-
# BAD: `delete_by` overrides an ActiveRecord method, but doesn't perform
22-
# any validation before passing its arguments on to another ActiveRecord method
2319
destroy_by(condition)
2420
end
2521
end
@@ -28,32 +24,17 @@ class FooController < ActionController::Base
2824

2925
MAX_USER_ID = 100_000
3026

31-
# A string tainted by user input is inserted into an SQL query
3227
def some_request_handler
33-
# BAD: executes `SELECT AVG(#{params[:column]}) FROM "users"`
34-
# where `params[:column]` is unsanitized
3528
User.calculate(:average, params[:column])
36-
37-
# BAD: executes `DELETE FROM "users" WHERE (id = '#{params[:id]}')`
38-
# where `params[:id]` is unsanitized
3929
User.delete_by("id = '#{params[:id]}'")
40-
41-
# BAD: executes `SELECT "users".* FROM "users" WHERE (id = '#{params[:id]}')`
42-
# where `params[:id]` is unsanitized
4330
User.destroy_by(["id = '#{params[:id]}'"])
44-
45-
# BAD: executes `SELECT "users".* FROM "users" WHERE id BETWEEN '#{params[:min_id]}' AND 100000`
46-
# where `params[:min_id]` is unsanitized
4731
User.where(<<-SQL, MAX_USER_ID)
4832
id BETWEEN '#{params[:min_id]}' AND ?
4933
SQL
50-
51-
# BAD: chained method case
52-
# executes `SELECT "users".* FROM "users" WHERE (NOT (user_id = 'params[:id]'))`
53-
# where `params[:id]` is unsanitized
5434
User.where.not("user.id = '#{params[:id]}'")
55-
5635
User.authenticate(params[:name], params[:pass])
36+
User.find_by_name("alice")
37+
User.not_a_find_by_method("bob")
5738
end
5839
end
5940

@@ -62,29 +43,22 @@ def some_other_request_handler
6243
ps = params
6344
uid = ps[:id]
6445
uidEq = "= '#{uid}'"
65-
66-
# BAD: executes `DELETE FROM "users" WHERE (id = #{uid})`
67-
# where `uid` is unsantized
6846
User.delete_by("id " + uidEq)
6947
end
7048

7149
def safe_paths
7250
dir = params[:order]
73-
# GOOD: barrier guard prevents taint flow
7451
dir = "DESC" unless dir == "ASC"
7552
User.order("name #{dir}")
7653

7754
name = params[:user_name]
78-
# GOOD: barrier guard prevents taint flow
7955
if %w(alice bob charlie).include? name
8056
User.find_by("username = #{name}")
8157
end
8258

8359
name = params[:user_name]
84-
# GOOD: hash arguments are sanitized by ActiveRecord
8560
User.find_by(user_name: name)
8661

87-
# OK: `find` method is overridden in `User`
8862
User.find(params[:user_group])
8963
end
9064
end
@@ -93,4 +67,4 @@ class BazController < BarController
9367
def yet_another_handler
9468
Admin.delete_by(params[:admin_condition])
9569
end
96-
end
70+
end

0 commit comments

Comments
 (0)