@@ -30,10 +30,8 @@ private predicate isBuiltInMethodForActiveRecordModelInstance(string methodName)
30
30
methodName = objectInstanceMethodName ( )
31
31
}
32
32
33
- private API:: Node activeRecordClassApiNode ( ) {
33
+ private API:: Node activeRecordBaseClass ( ) {
34
34
result =
35
- // class Foo < ActiveRecord::Base
36
- // class Bar < Foo
37
35
[
38
36
API:: getTopLevelMember ( "ActiveRecord" ) .getMember ( "Base" ) ,
39
37
// In Rails applications `ApplicationRecord` typically extends `ActiveRecord::Base`, but we
@@ -42,6 +40,46 @@ private API::Node activeRecordClassApiNode() {
42
40
]
43
41
}
44
42
43
+ /**
44
+ * Gets an object with methods from the ActiveRecord query interface.
45
+ */
46
+ private API:: Node activeRecordQueryBuilder ( ) {
47
+ result = activeRecordBaseClass ( )
48
+ or
49
+ result = activeRecordBaseClass ( ) .getInstance ( )
50
+ or
51
+ // Assume any method call might return an ActiveRecord::Relation
52
+ // These are dynamically generated
53
+ result = activeRecordQueryBuilderMethodAccess ( _) .getReturn ( )
54
+ }
55
+
56
+ /** Gets a call targeting the ActiveRecord query interface. */
57
+ private API:: MethodAccessNode activeRecordQueryBuilderMethodAccess ( string name ) {
58
+ result = activeRecordQueryBuilder ( ) .getMethod ( name ) and
59
+ // Due to the heuristic tracking of query builder objects, add a restriction for methods with a known call target
60
+ not isUnlikelyExternalCall ( result )
61
+ }
62
+
63
+ /** Gets a call targeting the ActiveRecord query interface. */
64
+ private DataFlow:: CallNode activeRecordQueryBuilderCall ( string name ) {
65
+ result = activeRecordQueryBuilderMethodAccess ( name ) .asCall ( )
66
+ }
67
+
68
+ /**
69
+ * Holds if `call` is unlikely to call into an external library, since it has a possible
70
+ * call target in its enclosing module.
71
+ */
72
+ private predicate isUnlikelyExternalCall ( API:: MethodAccessNode node ) {
73
+ exists ( DataFlow:: ModuleNode mod , DataFlow:: CallNode call | call = node .asCall ( ) |
74
+ call .getATarget ( ) = [ mod .getAnOwnSingletonMethod ( ) , mod .getAnOwnInstanceMethod ( ) ] and
75
+ call .getEnclosingMethod ( ) = [ mod .getAnOwnSingletonMethod ( ) , mod .getAnOwnInstanceMethod ( ) ]
76
+ )
77
+ }
78
+
79
+ private API:: Node activeRecordConnectionInstance ( ) {
80
+ result = activeRecordBaseClass ( ) .getReturn ( "connection" )
81
+ }
82
+
45
83
/**
46
84
* A `ClassDeclaration` for a class that inherits from `ActiveRecord::Base`. For example,
47
85
*
@@ -55,20 +93,19 @@ private API::Node activeRecordClassApiNode() {
55
93
* ```
56
94
*/
57
95
class ActiveRecordModelClass extends ClassDeclaration {
96
+ private DataFlow:: ClassNode cls ;
97
+
58
98
ActiveRecordModelClass ( ) {
59
- this .getSuperclassExpr ( ) =
60
- activeRecordClassApiNode ( ) .getASubclass ( ) .getAValueReachableFromSource ( ) .asExpr ( ) .getExpr ( )
99
+ cls = activeRecordBaseClass ( ) .getADescendentModule ( ) and this = cls .getADeclaration ( )
61
100
}
62
101
63
102
// Gets the class declaration for this class and all of its super classes
64
- private ModuleBase getAllClassDeclarations ( ) {
65
- result = this .getModule ( ) .getSuperClass * ( ) .getADeclaration ( )
66
- }
103
+ private ModuleBase getAllClassDeclarations ( ) { result = cls .getAnAncestor ( ) .getADeclaration ( ) }
67
104
68
105
/**
69
106
* Gets methods defined in this class that may access a field from the database.
70
107
*/
71
- Method getAPotentialFieldAccessMethod ( ) {
108
+ deprecated Method getAPotentialFieldAccessMethod ( ) {
72
109
// It's a method on this class or one of its super classes
73
110
result = this .getAllClassDeclarations ( ) .getAMethod ( ) and
74
111
// There is a value that can be returned by this method which may include field data
@@ -90,58 +127,84 @@ class ActiveRecordModelClass extends ClassDeclaration {
90
127
)
91
128
)
92
129
}
130
+
131
+ /** Gets the class as a `DataFlow::ClasNode`. */
132
+ DataFlow:: ClassNode getClassNode ( ) { result = cls }
93
133
}
94
134
95
- /** A class method call whose receiver is an `ActiveRecordModelClass`. */
96
- class ActiveRecordModelClassMethodCall extends MethodCall {
97
- private ActiveRecordModelClass recvCls ;
135
+ /**
136
+ * Gets a potential reference to an ActiveRecord class object.
137
+ */
138
+ deprecated private API:: Node getAnActiveRecordModelClassRef ( ) {
139
+ result = any ( ActiveRecordModelClass cls ) .getClassNode ( ) .trackModule ( )
140
+ or
141
+ // For methods with an unknown call target, assume this might be a database field, thus returning another ActiveRecord object.
142
+ // In this case we do not know which class it belongs to, which is why this predicate can't associate the reference with a specific class.
143
+ result = getAnUnknownActiveRecordModelClassCall ( ) .getReturn ( )
144
+ }
98
145
146
+ /**
147
+ * Gets a call performed on an ActiveRecord class object, without a known call target in the codebase.
148
+ */
149
+ deprecated private API:: MethodAccessNode getAnUnknownActiveRecordModelClassCall ( ) {
150
+ result = getAnActiveRecordModelClassRef ( ) .getMethod ( _) and
151
+ result .asCall ( ) .asExpr ( ) .getExpr ( ) instanceof UnknownMethodCall
152
+ }
153
+
154
+ /**
155
+ * DEPRECATED. Use `ActiveRecordModelClass.getClassNode().trackModule().getMethod()` instead.
156
+ *
157
+ * A class method call whose receiver is an `ActiveRecordModelClass`.
158
+ */
159
+ deprecated class ActiveRecordModelClassMethodCall extends MethodCall {
99
160
ActiveRecordModelClassMethodCall ( ) {
100
- // e.g. Foo.where(...)
101
- recvCls .getModule ( ) = this .getReceiver ( ) .( ConstantReadAccess ) .getModule ( )
102
- or
103
- // e.g. Foo.joins(:bars).where(...)
104
- recvCls = this .getReceiver ( ) .( ActiveRecordModelClassMethodCall ) .getReceiverClass ( )
105
- or
106
- // e.g. self.where(...) within an ActiveRecordModelClass
107
- this .getReceiver ( ) instanceof SelfVariableAccess and
108
- this .getEnclosingModule ( ) = recvCls
161
+ this = getAnUnknownActiveRecordModelClassCall ( ) .asCall ( ) .asExpr ( ) .getExpr ( )
109
162
}
110
163
111
- /** The `ActiveRecordModelClass` of the receiver of this method. */
112
- ActiveRecordModelClass getReceiverClass ( ) { result = recvCls }
164
+ /** Gets the `ActiveRecordModelClass` of the receiver of this method, if it can be determined. */
165
+ ActiveRecordModelClass getReceiverClass ( ) {
166
+ this = result .getClassNode ( ) .trackModule ( ) .getMethod ( _) .asCall ( ) .asExpr ( ) .getExpr ( )
167
+ }
113
168
}
114
169
115
- private Expr sqlFragmentArgument ( MethodCall call ) {
116
- exists ( string methodName |
117
- methodName = call .getMethodName ( ) and
118
- (
119
- methodName =
120
- [
121
- "delete_all" , "delete_by" , "destroy_all" , "destroy_by" , "exists?" , "find_by" , "find_by!" ,
122
- "find_or_create_by" , "find_or_create_by!" , "find_or_initialize_by" , "find_by_sql" , "from" ,
123
- "group" , "having" , "joins" , "lock" , "not" , "order" , "reorder" , "pluck" , "where" ,
124
- "rewhere" , "select" , "reselect" , "update_all"
125
- ] and
126
- result = call .getArgument ( 0 )
127
- or
128
- methodName = "calculate" and result = call .getArgument ( 1 )
129
- or
130
- methodName in [ "average" , "count" , "maximum" , "minimum" , "sum" , "count_by_sql" ] and
131
- result = call .getArgument ( 0 )
132
- or
133
- // This format was supported until Rails 2.3.8
134
- methodName = [ "all" , "find" , "first" , "last" ] and
135
- result = call .getKeywordArgument ( "conditions" )
136
- or
137
- methodName = "reload" and
138
- result = call .getKeywordArgument ( "lock" )
139
- or
140
- // Calls to `annotate` can be used to add block comments to SQL queries. These are potentially vulnerable to
141
- // SQLi if user supplied input is passed in as an argument.
142
- methodName = "annotate" and
143
- result = call .getArgument ( _)
144
- )
170
+ private predicate sqlFragmentArgumentInner ( DataFlow:: CallNode call , DataFlow:: Node sink ) {
171
+ call =
172
+ activeRecordQueryBuilderCall ( [
173
+ "delete_all" , "delete_by" , "destroy_all" , "destroy_by" , "exists?" , "find_by" , "find_by!" ,
174
+ "find_or_create_by" , "find_or_create_by!" , "find_or_initialize_by" , "find_by_sql" , "from" ,
175
+ "group" , "having" , "joins" , "lock" , "not" , "order" , "reorder" , "pluck" , "where" , "rewhere" ,
176
+ "select" , "reselect" , "update_all"
177
+ ] ) and
178
+ sink = call .getArgument ( 0 )
179
+ or
180
+ call = activeRecordQueryBuilderCall ( "calculate" ) and
181
+ sink = call .getArgument ( 1 )
182
+ or
183
+ call =
184
+ activeRecordQueryBuilderCall ( [ "average" , "count" , "maximum" , "minimum" , "sum" , "count_by_sql" ] ) and
185
+ sink = call .getArgument ( 0 )
186
+ or
187
+ // This format was supported until Rails 2.3.8
188
+ call = activeRecordQueryBuilderCall ( [ "all" , "find" , "first" , "last" ] ) and
189
+ sink = call .getKeywordArgument ( "conditions" )
190
+ or
191
+ call = activeRecordQueryBuilderCall ( "reload" ) and
192
+ sink = call .getKeywordArgument ( "lock" )
193
+ or
194
+ // Calls to `annotate` can be used to add block comments to SQL queries. These are potentially vulnerable to
195
+ // SQLi if user supplied input is passed in as an argument.
196
+ call = activeRecordQueryBuilderCall ( "annotate" ) and
197
+ sink = call .getArgument ( _)
198
+ or
199
+ call = activeRecordConnectionInstance ( ) .getAMethodCall ( "execute" ) and
200
+ sink = call .getArgument ( 0 )
201
+ }
202
+
203
+ private predicate sqlFragmentArgument ( DataFlow:: CallNode call , DataFlow:: Node sink ) {
204
+ exists ( DataFlow:: Node arg |
205
+ sqlFragmentArgumentInner ( call , arg ) and
206
+ sink = [ arg , arg .( DataFlow:: ArrayLiteralNode ) .getElement ( 0 ) ] and
207
+ unsafeSqlExpr ( sink .asExpr ( ) .getExpr ( ) )
145
208
)
146
209
}
147
210
@@ -162,6 +225,8 @@ private predicate unsafeSqlExpr(Expr sqlFragmentExpr) {
162
225
}
163
226
164
227
/**
228
+ * DEPRECATED. Use the `SqlExecution` concept or `ActiveRecordSqlExecutionRange`.
229
+ *
165
230
* A method call that may result in executing unintended user-controlled SQL
166
231
* queries if the `getSqlFragmentSinkArgument()` expression is tainted by
167
232
* unsanitized user-controlled input. For example, supposing that `User` is an
@@ -175,55 +240,32 @@ private predicate unsafeSqlExpr(Expr sqlFragmentExpr) {
175
240
* as `"') OR 1=1 --"` could result in the application looking up all users
176
241
* rather than just one with a matching name.
177
242
*/
178
- class PotentiallyUnsafeSqlExecutingMethodCall extends ActiveRecordModelClassMethodCall {
179
- // The SQL fragment argument itself
180
- private Expr sqlFragmentExpr ;
243
+ deprecated class PotentiallyUnsafeSqlExecutingMethodCall extends ActiveRecordModelClassMethodCall {
244
+ private DataFlow:: CallNode call ;
181
245
182
246
PotentiallyUnsafeSqlExecutingMethodCall ( ) {
183
- exists ( Expr arg |
184
- arg = sqlFragmentArgument ( this ) and
185
- unsafeSqlExpr ( sqlFragmentExpr ) and
186
- (
187
- sqlFragmentExpr = arg
188
- or
189
- sqlFragmentExpr = arg .( ArrayLiteral ) .getElement ( 0 )
190
- ) and
191
- // Check that method has not been overridden
192
- not exists ( SingletonMethod m |
193
- m .getName ( ) = this .getMethodName ( ) and
194
- m .getOuterScope ( ) = this .getReceiverClass ( )
195
- )
196
- )
247
+ call .asExpr ( ) .getExpr ( ) = this and sqlFragmentArgument ( call , _)
197
248
}
198
249
199
250
/**
200
251
* Gets the SQL fragment argument of this method call.
201
252
*/
202
- Expr getSqlFragmentSinkArgument ( ) { result = sqlFragmentExpr }
253
+ Expr getSqlFragmentSinkArgument ( ) {
254
+ exists ( DataFlow:: Node sink |
255
+ sqlFragmentArgument ( call , sink ) and result = sink .asExpr ( ) .getExpr ( )
256
+ )
257
+ }
203
258
}
204
259
205
260
/**
206
- * An `SqlExecution::Range` for an argument to a
207
- * `PotentiallyUnsafeSqlExecutingMethodCall` that may be vulnerable to being
208
- * controlled by user input.
261
+ * A SQL execution arising from a call to the ActiveRecord library.
209
262
*/
210
263
class ActiveRecordSqlExecutionRange extends SqlExecution:: Range {
211
- ActiveRecordSqlExecutionRange ( ) {
212
- exists ( PotentiallyUnsafeSqlExecutingMethodCall mc |
213
- this .asExpr ( ) .getNode ( ) = mc .getSqlFragmentSinkArgument ( )
214
- )
215
- or
216
- this = activeRecordConnectionInstance ( ) .getAMethodCall ( "execute" ) .getArgument ( 0 ) and
217
- unsafeSqlExpr ( this .asExpr ( ) .getExpr ( ) )
218
- }
264
+ ActiveRecordSqlExecutionRange ( ) { sqlFragmentArgument ( _, this ) }
219
265
220
266
override DataFlow:: Node getSql ( ) { result = this }
221
267
}
222
268
223
- private API:: Node activeRecordConnectionInstance ( ) {
224
- result = activeRecordClassApiNode ( ) .getReturn ( "connection" )
225
- }
226
-
227
269
// TODO: model `ActiveRecord` sanitizers
228
270
// https://api.rubyonrails.org/classes/ActiveRecord/Sanitization/ClassMethods.html
229
271
/**
@@ -241,15 +283,8 @@ abstract class ActiveRecordModelInstantiation extends OrmInstantiation::Range,
241
283
override predicate methodCallMayAccessField ( string methodName ) {
242
284
// The method is not a built-in, and...
243
285
not isBuiltInMethodForActiveRecordModelInstance ( methodName ) and
244
- (
245
- // ...There is no matching method definition in the class, or...
246
- not exists ( this .getClass ( ) .getMethod ( methodName ) )
247
- or
248
- // ...the called method can access a field.
249
- exists ( Method m | m = this .getClass ( ) .getAPotentialFieldAccessMethod ( ) |
250
- m .getName ( ) = methodName
251
- )
252
- )
286
+ // ...There is no matching method definition in the class
287
+ not exists ( this .getClass ( ) .getMethod ( methodName ) )
253
288
}
254
289
}
255
290
@@ -317,21 +352,10 @@ private class ActiveRecordModelFinderCall extends ActiveRecordModelInstantiation
317
352
}
318
353
319
354
// A `self` reference that may resolve to an active record model object
320
- private class ActiveRecordModelClassSelfReference extends ActiveRecordModelInstantiation ,
321
- DataFlow:: SelfParameterNode
322
- {
355
+ private class ActiveRecordModelClassSelfReference extends ActiveRecordModelInstantiation {
323
356
private ActiveRecordModelClass cls ;
324
357
325
- ActiveRecordModelClassSelfReference ( ) {
326
- exists ( MethodBase m |
327
- m = this .getCallable ( ) and
328
- m .getEnclosingModule ( ) = cls and
329
- m = cls .getAMethod ( )
330
- ) and
331
- // In a singleton method, `self` refers to the class itself rather than an
332
- // instance of that class
333
- not this .getSelfVariable ( ) .getDeclaringScope ( ) instanceof SingletonMethod
334
- }
358
+ ActiveRecordModelClassSelfReference ( ) { this = cls .getClassNode ( ) .getAnOwnInstanceSelf ( ) }
335
359
336
360
final override ActiveRecordModelClass getClass ( ) { result = cls }
337
361
}
@@ -342,7 +366,7 @@ private class ActiveRecordModelClassSelfReference extends ActiveRecordModelInsta
342
366
class ActiveRecordInstance extends DataFlow:: Node {
343
367
private ActiveRecordModelInstantiation instantiation ;
344
368
345
- ActiveRecordInstance ( ) { this = instantiation or instantiation . flowsTo ( this ) }
369
+ ActiveRecordInstance ( ) { this = instantiation . track ( ) . getAValueReachableFromSource ( ) }
346
370
347
371
/** Gets the `ActiveRecordModelClass` that this is an instance of. */
348
372
ActiveRecordModelClass getClass ( ) { result = instantiation .getClass ( ) }
@@ -380,12 +404,12 @@ private module Persistence {
380
404
/** A call to e.g. `User.create(name: "foo")` */
381
405
private class CreateLikeCall extends DataFlow:: CallNode , PersistentWriteAccess:: Range {
382
406
CreateLikeCall ( ) {
383
- exists ( this . asExpr ( ) . getExpr ( ) . ( ActiveRecordModelClassMethodCall ) . getReceiverClass ( ) ) and
384
- this . getMethodName ( ) =
385
- [
386
- "create" , "create!" , "create_or_find_by" , "create_or_find_by!" , "find_or_create_by" ,
387
- "find_or_create_by!" , "insert" , "insert!"
388
- ]
407
+ this =
408
+ activeRecordBaseClass ( )
409
+ . getAMethodCall ( [
410
+ "create" , "create!" , "create_or_find_by" , "create_or_find_by!" , "find_or_create_by" ,
411
+ "find_or_create_by!" , "insert" , "insert!"
412
+ ] )
389
413
}
390
414
391
415
override DataFlow:: Node getValue ( ) {
@@ -402,8 +426,7 @@ private module Persistence {
402
426
/** A call to e.g. `User.update(1, name: "foo")` */
403
427
private class UpdateLikeClassMethodCall extends DataFlow:: CallNode , PersistentWriteAccess:: Range {
404
428
UpdateLikeClassMethodCall ( ) {
405
- exists ( this .asExpr ( ) .getExpr ( ) .( ActiveRecordModelClassMethodCall ) .getReceiverClass ( ) ) and
406
- this .getMethodName ( ) = [ "update" , "update!" , "upsert" ]
429
+ this = activeRecordBaseClass ( ) .getAMethodCall ( [ "update" , "update!" , "upsert" ] )
407
430
}
408
431
409
432
override DataFlow:: Node getValue ( ) {
@@ -448,10 +471,7 @@ private module Persistence {
448
471
* ```
449
472
*/
450
473
private class TouchAllCall extends DataFlow:: CallNode , PersistentWriteAccess:: Range {
451
- TouchAllCall ( ) {
452
- exists ( this .asExpr ( ) .getExpr ( ) .( ActiveRecordModelClassMethodCall ) .getReceiverClass ( ) ) and
453
- this .getMethodName ( ) = "touch_all"
454
- }
474
+ TouchAllCall ( ) { this = activeRecordQueryBuilderCall ( "touch_all" ) }
455
475
456
476
override DataFlow:: Node getValue ( ) { result = this .getKeywordArgument ( "time" ) }
457
477
}
@@ -461,8 +481,7 @@ private module Persistence {
461
481
private ExprNodes:: ArrayLiteralCfgNode arr ;
462
482
463
483
InsertAllLikeCall ( ) {
464
- exists ( this .asExpr ( ) .getExpr ( ) .( ActiveRecordModelClassMethodCall ) .getReceiverClass ( ) ) and
465
- this .getMethodName ( ) = [ "insert_all" , "insert_all!" , "upsert_all" ] and
484
+ this = activeRecordBaseClass ( ) .getAMethodCall ( [ "insert_all" , "insert_all!" , "upsert_all" ] ) and
466
485
arr = this .getArgument ( 0 ) .asExpr ( )
467
486
}
468
487
0 commit comments