Skip to content

Commit 83393dc

Browse files
committed
Ruby: Recognise more AR write accesses
This change means we recognise calls like ```rb User.create(params) User.update(id, params) ``` as instances of `PersistentWriteAccess`.
1 parent 21b4918 commit 83393dc

File tree

8 files changed

+100
-28
lines changed

8 files changed

+100
-28
lines changed

ruby/ql/lib/codeql/ruby/controlflow/CfgNodes.qll

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,7 @@ module ExprNodes {
358358
}
359359

360360
/**
361-
* Gets the `nth` positional argument of this call.
361+
* Gets the `n`th positional argument of this call.
362362
* Unlike `getArgument`, this excludes keyword arguments.
363363
*/
364364
final ExprCfgNode getPositionalArgument(int n) {

ruby/ql/lib/codeql/ruby/dataflow/internal/DataFlowPublic.qll

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ class CallNode extends LocalSourceNode, ExprNode {
7272
ExprNode getKeywordArgument(string name) { result.getExprNode() = node.getKeywordArgument(name) }
7373

7474
/**
75-
* Gets the `nth` positional argument of this call.
75+
* Gets the `n`th positional argument of this call.
7676
* Unlike `getArgument`, this excludes keyword arguments.
7777
*/
7878
final ExprNode getPositionalArgument(int n) {

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

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -364,15 +364,6 @@ private module Persistence {
364364
)
365365
}
366366

367-
/**
368-
* Holds if `call` has a keyword argument with value `value`.
369-
*/
370-
private predicate keywordArgumentWithValue(DataFlow::CallNode call, DataFlow::ExprNode value) {
371-
exists(ExprNodes::PairCfgNode pair | pair = call.getArgument(_).asExpr() |
372-
value.asExpr() = pair.getValue()
373-
)
374-
}
375-
376367
/** A call to e.g. `User.create(name: "foo")` */
377368
private class CreateLikeCall extends DataFlow::CallNode, PersistentWriteAccess::Range {
378369
CreateLikeCall() {
@@ -386,8 +377,12 @@ private module Persistence {
386377

387378
override DataFlow::Node getValue() {
388379
// attrs as hash elements in arg0
389-
hashArgumentWithValue(this, 0, result) or
390-
keywordArgumentWithValue(this, result)
380+
hashArgumentWithValue(this, 0, result)
381+
or
382+
result = this.getKeywordArgument(_)
383+
or
384+
result = this.getPositionalArgument(0) and
385+
not result.asExpr() instanceof ExprNodes::HashLiteralCfgNode
391386
}
392387
}
393388

@@ -399,11 +394,19 @@ private module Persistence {
399394
}
400395

401396
override DataFlow::Node getValue() {
402-
keywordArgumentWithValue(this, result)
397+
// User.update(1, name: "foo")
398+
result = this.getKeywordArgument(_)
399+
or
400+
// User.update(1, params)
401+
exists(int n | n > 0 |
402+
result = this.getPositionalArgument(n) and
403+
not result.asExpr() instanceof ExprNodes::ArrayLiteralCfgNode
404+
)
403405
or
404406
// Case where 2 array args are passed - the first an array of IDs, and the
405407
// second an array of hashes - each hash corresponding to an ID in the
406408
// first array.
409+
// User.update([1,2,3], [{name: "foo"}, {name: "bar"}])
407410
exists(ExprNodes::ArrayLiteralCfgNode hashesArray |
408411
this.getArgument(0).asExpr() instanceof ExprNodes::ArrayLiteralCfgNode and
409412
hashesArray = this.getArgument(1).asExpr()
@@ -472,8 +475,12 @@ private module Persistence {
472475
// attrs as hash elements in arg0
473476
hashArgumentWithValue(this, 0, result)
474477
or
478+
// attrs as variable in arg0
479+
result = this.getPositionalArgument(0) and
480+
not result.asExpr() instanceof ExprNodes::HashLiteralCfgNode
481+
or
475482
// keyword arg
476-
keywordArgumentWithValue(this, result)
483+
result = this.getKeywordArgument(_)
477484
}
478485
}
479486

ruby/ql/test/library-tests/concepts/PersistentWriteAccess.expected

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@
1515
| app/controllers/users_controller.rb:23:7:23:42 | call to update_attribute | app/controllers/users_controller.rb:23:37:23:41 | "U13" |
1616
| app/controllers/users_controller.rb:26:19:26:23 | ... = ... | app/controllers/users_controller.rb:26:19:26:23 | "U14" |
1717
| app/controllers/users_controller.rb:31:7:31:32 | call to touch_all | app/controllers/users_controller.rb:31:28:31:31 | call to time |
18+
| app/controllers/users_controller.rb:35:7:35:27 | call to update | app/controllers/users_controller.rb:35:22:35:26 | attrs |
19+
| app/controllers/users_controller.rb:36:7:36:28 | call to update! | app/controllers/users_controller.rb:36:23:36:27 | attrs |
20+
| app/controllers/users_controller.rb:39:7:39:24 | call to create | app/controllers/users_controller.rb:39:19:39:23 | attrs |
21+
| app/controllers/users_controller.rb:40:7:40:25 | call to create! | app/controllers/users_controller.rb:40:20:40:24 | attrs |
22+
| app/controllers/users_controller.rb:41:7:41:24 | call to insert | app/controllers/users_controller.rb:41:19:41:23 | attrs |
1823
| app/models/user.rb:4:5:4:28 | call to update | app/models/user.rb:4:23:4:27 | "U15" |
1924
| app/models/user.rb:5:5:5:23 | call to update | app/models/user.rb:5:18:5:22 | "U16" |
2025
| app/models/user.rb:6:5:6:56 | call to update_attributes | app/models/user.rb:6:35:6:39 | "U17" |

ruby/ql/test/library-tests/concepts/app/controllers/users_controller.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@ def create_or_modify
2929
# TouchAllCall
3030
User.touch_all
3131
User.touch_all(time: time)
32+
33+
# UpdateLikeClassMethodCall
34+
attrs = {name: "U15"}
35+
User.update(8, attrs)
36+
User.update!(8, attrs)
37+
38+
# CreateLikeClassMethodCall
39+
User.create(attrs)
40+
User.create!(attrs)
41+
User.insert(attrs)
3242
end
3343

3444
def get_uid

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

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
actionControllerControllerClasses
22
| active_record/ActiveRecord.rb:23:1:39:3 | FooController |
33
| active_record/ActiveRecord.rb:41:1:64:3 | BarController |
4-
| active_record/ActiveRecord.rb:66:1:70:3 | BazController |
5-
| active_record/ActiveRecord.rb:72:1:80:3 | AnnotatedController |
4+
| active_record/ActiveRecord.rb:66:1:94:3 | BazController |
5+
| active_record/ActiveRecord.rb:96:1:104:3 | AnnotatedController |
66
| app/controllers/comments_controller.rb:1:1:7:3 | CommentsController |
77
| app/controllers/foo/bars_controller.rb:3:1:39:3 | BarsController |
88
| app/controllers/photos_controller.rb:1:1:4:3 | PhotosController |
@@ -13,8 +13,14 @@ actionControllerActionMethods
1313
| active_record/ActiveRecord.rb:42:3:47:5 | some_other_request_handler |
1414
| active_record/ActiveRecord.rb:49:3:63:5 | safe_paths |
1515
| active_record/ActiveRecord.rb:67:3:69:5 | yet_another_handler |
16-
| active_record/ActiveRecord.rb:73:3:75:5 | index |
17-
| active_record/ActiveRecord.rb:77:3:79:5 | unsafe_action |
16+
| active_record/ActiveRecord.rb:71:3:73:5 | create1 |
17+
| active_record/ActiveRecord.rb:75:3:77:5 | create2 |
18+
| active_record/ActiveRecord.rb:79:3:81:5 | create3 |
19+
| active_record/ActiveRecord.rb:83:3:85:5 | update1 |
20+
| active_record/ActiveRecord.rb:87:3:89:5 | update2 |
21+
| active_record/ActiveRecord.rb:91:3:93:5 | update3 |
22+
| active_record/ActiveRecord.rb:97:3:99:5 | index |
23+
| active_record/ActiveRecord.rb:101:3:103:5 | unsafe_action |
1824
| app/controllers/comments_controller.rb:2:3:3:5 | index |
1925
| app/controllers/comments_controller.rb:5:3:6:5 | show |
2026
| app/controllers/foo/bars_controller.rb:5:3:7:5 | index |
@@ -41,7 +47,17 @@ paramsCalls
4147
| active_record/ActiveRecord.rb:59:12:59:17 | call to params |
4248
| active_record/ActiveRecord.rb:62:15:62:20 | call to params |
4349
| active_record/ActiveRecord.rb:68:21:68:26 | call to params |
44-
| active_record/ActiveRecord.rb:78:59:78:64 | call to params |
50+
| active_record/ActiveRecord.rb:72:18:72:23 | call to params |
51+
| active_record/ActiveRecord.rb:76:24:76:29 | call to params |
52+
| active_record/ActiveRecord.rb:76:49:76:54 | call to params |
53+
| active_record/ActiveRecord.rb:80:25:80:30 | call to params |
54+
| active_record/ActiveRecord.rb:80:50:80:55 | call to params |
55+
| active_record/ActiveRecord.rb:84:21:84:26 | call to params |
56+
| active_record/ActiveRecord.rb:88:27:88:32 | call to params |
57+
| active_record/ActiveRecord.rb:88:52:88:57 | call to params |
58+
| active_record/ActiveRecord.rb:92:28:92:33 | call to params |
59+
| active_record/ActiveRecord.rb:92:53:92:58 | call to params |
60+
| active_record/ActiveRecord.rb:102:59:102:64 | call to params |
4561
| app/controllers/foo/bars_controller.rb:13:21:13:26 | call to params |
4662
| app/controllers/foo/bars_controller.rb:14:10:14:15 | call to params |
4763
| app/controllers/foo/bars_controller.rb:21:21:21:26 | call to params |
@@ -61,7 +77,17 @@ paramsSources
6177
| active_record/ActiveRecord.rb:59:12:59:17 | call to params |
6278
| active_record/ActiveRecord.rb:62:15:62:20 | call to params |
6379
| active_record/ActiveRecord.rb:68:21:68:26 | call to params |
64-
| active_record/ActiveRecord.rb:78:59:78:64 | call to params |
80+
| active_record/ActiveRecord.rb:72:18:72:23 | call to params |
81+
| active_record/ActiveRecord.rb:76:24:76:29 | call to params |
82+
| active_record/ActiveRecord.rb:76:49:76:54 | call to params |
83+
| active_record/ActiveRecord.rb:80:25:80:30 | call to params |
84+
| active_record/ActiveRecord.rb:80:50:80:55 | call to params |
85+
| active_record/ActiveRecord.rb:84:21:84:26 | call to params |
86+
| active_record/ActiveRecord.rb:88:27:88:32 | call to params |
87+
| active_record/ActiveRecord.rb:88:52:88:57 | call to params |
88+
| active_record/ActiveRecord.rb:92:28:92:33 | call to params |
89+
| active_record/ActiveRecord.rb:92:53:92:58 | call to params |
90+
| active_record/ActiveRecord.rb:102:59:102:64 | call to params |
6591
| app/controllers/foo/bars_controller.rb:13:21:13:26 | call to params |
6692
| app/controllers/foo/bars_controller.rb:14:10:14:15 | call to params |
6793
| app/controllers/foo/bars_controller.rb:21:21:21:26 | call to params |

ruby/ql/test/library-tests/frameworks/active_record/ActiveRecord.expected

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ activeRecordSqlExecutionRanges
2222
| ActiveRecord.rb:46:20:46:32 | ... + ... |
2323
| ActiveRecord.rb:52:16:52:28 | "name #{...}" |
2424
| ActiveRecord.rb:56:20:56:39 | "username = #{...}" |
25-
| ActiveRecord.rb:78:27:78:76 | "this is an unsafe annotation:..." |
25+
| ActiveRecord.rb:102:27:102:76 | "this is an unsafe annotation:..." |
2626
activeRecordModelClassMethodCalls
2727
| ActiveRecord.rb:2:3:2:17 | call to has_many |
2828
| ActiveRecord.rb:6:3:6:24 | call to belongs_to |
@@ -45,8 +45,14 @@ activeRecordModelClassMethodCalls
4545
| ActiveRecord.rb:60:5:60:33 | call to find_by |
4646
| ActiveRecord.rb:62:5:62:34 | call to find |
4747
| ActiveRecord.rb:68:5:68:45 | call to delete_by |
48-
| ActiveRecord.rb:74:13:74:54 | call to annotate |
49-
| ActiveRecord.rb:78:13:78:77 | call to annotate |
48+
| ActiveRecord.rb:72:5:72:24 | call to create |
49+
| ActiveRecord.rb:76:5:76:66 | call to create |
50+
| ActiveRecord.rb:80:5:80:68 | call to create |
51+
| ActiveRecord.rb:84:5:84:27 | call to update |
52+
| ActiveRecord.rb:88:5:88:69 | call to update |
53+
| ActiveRecord.rb:92:5:92:71 | call to update |
54+
| ActiveRecord.rb:98:13:98:54 | call to annotate |
55+
| ActiveRecord.rb:102:13:102:77 | call to annotate |
5056
potentiallyUnsafeSqlExecutingMethodCall
5157
| ActiveRecord.rb:9:5:9:68 | call to find |
5258
| ActiveRecord.rb:19:5:19:25 | call to destroy_by |
@@ -58,11 +64,21 @@ potentiallyUnsafeSqlExecutingMethodCall
5864
| ActiveRecord.rb:46:5:46:33 | call to delete_by |
5965
| ActiveRecord.rb:52:5:52:29 | call to order |
6066
| ActiveRecord.rb:56:7:56:40 | call to find_by |
61-
| ActiveRecord.rb:78:13:78:77 | call to annotate |
67+
| ActiveRecord.rb:102:13:102:77 | call to annotate |
6268
activeRecordModelInstantiations
6369
| ActiveRecord.rb:9:5:9:68 | call to find | ActiveRecord.rb:5:1:15:3 | User |
6470
| ActiveRecord.rb:13:5:13:40 | call to find_by | ActiveRecord.rb:1:1:3:3 | UserGroup |
6571
| ActiveRecord.rb:36:5:36:30 | call to find_by_name | ActiveRecord.rb:5:1:15:3 | User |
6672
| ActiveRecord.rb:56:7:56:40 | call to find_by | ActiveRecord.rb:5:1:15:3 | User |
6773
| ActiveRecord.rb:60:5:60:33 | call to find_by | ActiveRecord.rb:5:1:15:3 | User |
6874
| ActiveRecord.rb:62:5:62:34 | call to find | ActiveRecord.rb:5:1:15:3 | User |
75+
persistentWriteAccesses
76+
| ActiveRecord.rb:72:5:72:24 | call to create | ActiveRecord.rb:72:18:72:23 | call to params |
77+
| ActiveRecord.rb:76:5:76:66 | call to create | ActiveRecord.rb:76:24:76:36 | ...[...] |
78+
| ActiveRecord.rb:76:5:76:66 | call to create | ActiveRecord.rb:76:49:76:65 | ...[...] |
79+
| ActiveRecord.rb:80:5:80:68 | call to create | ActiveRecord.rb:80:25:80:37 | ...[...] |
80+
| ActiveRecord.rb:80:5:80:68 | call to create | ActiveRecord.rb:80:50:80:66 | ...[...] |
81+
| ActiveRecord.rb:84:5:84:27 | call to update | ActiveRecord.rb:84:21:84:26 | call to params |
82+
| ActiveRecord.rb:88:5:88:69 | call to update | ActiveRecord.rb:88:27:88:39 | ...[...] |
83+
| ActiveRecord.rb:88:5:88:69 | call to update | ActiveRecord.rb:88:52:88:68 | ...[...] |
84+
| ActiveRecord.rb:92:5:92:71 | call to update | ActiveRecord.rb:92:21:92:70 | call to [] |

ruby/ql/test/library-tests/frameworks/active_record/ActiveRecord.rb

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,15 +73,23 @@ def create1
7373
end
7474

7575
def create2
76-
Admin.create(name: params[:name])
76+
Admin.create(name: params[:name], password: params[:password])
77+
end
78+
79+
def create3
80+
Admin.create({name: params[:name], password: params[:password]})
7781
end
7882

7983
def update1
80-
Admin.update(params)
84+
Admin.update(1, params)
8185
end
8286

8387
def update2
84-
Admin.update(name: params[:name])
88+
Admin.update(1, name: params[:name], password: params[:password])
89+
end
90+
91+
def update3
92+
Admin.update(1, {name: params[:name], password: params[:password]})
8593
end
8694
end
8795

0 commit comments

Comments
 (0)