@@ -30,10 +30,8 @@ private predicate isBuiltInMethodForActiveRecordModelInstance(string methodName)
3030 methodName = objectInstanceMethodName ( )
3131}
3232
33- private API:: Node activeRecordClassApiNode ( ) {
33+ private API:: Node activeRecordBaseClass ( ) {
3434 result =
35- // class Foo < ActiveRecord::Base
36- // class Bar < Foo
3735 [
3836 API:: getTopLevelMember ( "ActiveRecord" ) .getMember ( "Base" ) ,
3937 // In Rails applications `ApplicationRecord` typically extends `ActiveRecord::Base`, but we
@@ -42,6 +40,46 @@ private API::Node activeRecordClassApiNode() {
4240 ]
4341}
4442
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+
4583/**
4684 * A `ClassDeclaration` for a class that inherits from `ActiveRecord::Base`. For example,
4785 *
@@ -55,20 +93,19 @@ private API::Node activeRecordClassApiNode() {
5593 * ```
5694 */
5795class ActiveRecordModelClass extends ClassDeclaration {
96+ private DataFlow:: ClassNode cls ;
97+
5898 ActiveRecordModelClass ( ) {
59- this .getSuperclassExpr ( ) =
60- activeRecordClassApiNode ( ) .getASubclass ( ) .getAValueReachableFromSource ( ) .asExpr ( ) .getExpr ( )
99+ cls = activeRecordBaseClass ( ) .getADescendentModule ( ) and this = cls .getADeclaration ( )
61100 }
62101
63102 // 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 ( ) }
67104
68105 /**
69106 * Gets methods defined in this class that may access a field from the database.
70107 */
71- Method getAPotentialFieldAccessMethod ( ) {
108+ deprecated Method getAPotentialFieldAccessMethod ( ) {
72109 // It's a method on this class or one of its super classes
73110 result = this .getAllClassDeclarations ( ) .getAMethod ( ) and
74111 // There is a value that can be returned by this method which may include field data
@@ -90,58 +127,84 @@ class ActiveRecordModelClass extends ClassDeclaration {
90127 )
91128 )
92129 }
130+
131+ /** Gets the class as a `DataFlow::ClasNode`. */
132+ DataFlow:: ClassNode getClassNode ( ) { result = cls }
93133}
94134
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+ }
98145
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 {
99160 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 ( )
109162 }
110163
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+ }
113168}
114169
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 ( ) )
145208 )
146209}
147210
@@ -162,6 +225,8 @@ private predicate unsafeSqlExpr(Expr sqlFragmentExpr) {
162225}
163226
164227/**
228+ * DEPRECATED. Use the `SqlExecution` concept or `ActiveRecordSqlExecutionRange`.
229+ *
165230 * A method call that may result in executing unintended user-controlled SQL
166231 * queries if the `getSqlFragmentSinkArgument()` expression is tainted by
167232 * unsanitized user-controlled input. For example, supposing that `User` is an
@@ -175,55 +240,32 @@ private predicate unsafeSqlExpr(Expr sqlFragmentExpr) {
175240 * as `"') OR 1=1 --"` could result in the application looking up all users
176241 * rather than just one with a matching name.
177242 */
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 ;
181245
182246 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 , _)
197248 }
198249
199250 /**
200251 * Gets the SQL fragment argument of this method call.
201252 */
202- Expr getSqlFragmentSinkArgument ( ) { result = sqlFragmentExpr }
253+ Expr getSqlFragmentSinkArgument ( ) {
254+ exists ( DataFlow:: Node sink |
255+ sqlFragmentArgument ( call , sink ) and result = sink .asExpr ( ) .getExpr ( )
256+ )
257+ }
203258}
204259
205260/**
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.
209262 */
210263class 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 ) }
219265
220266 override DataFlow:: Node getSql ( ) { result = this }
221267}
222268
223- private API:: Node activeRecordConnectionInstance ( ) {
224- result = activeRecordClassApiNode ( ) .getReturn ( "connection" )
225- }
226-
227269// TODO: model `ActiveRecord` sanitizers
228270// https://api.rubyonrails.org/classes/ActiveRecord/Sanitization/ClassMethods.html
229271/**
@@ -241,15 +283,8 @@ abstract class ActiveRecordModelInstantiation extends OrmInstantiation::Range,
241283 override predicate methodCallMayAccessField ( string methodName ) {
242284 // The method is not a built-in, and...
243285 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 ) )
253288 }
254289}
255290
@@ -317,21 +352,10 @@ private class ActiveRecordModelFinderCall extends ActiveRecordModelInstantiation
317352}
318353
319354// 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 {
323356 private ActiveRecordModelClass cls ;
324357
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 ( ) }
335359
336360 final override ActiveRecordModelClass getClass ( ) { result = cls }
337361}
@@ -342,7 +366,7 @@ private class ActiveRecordModelClassSelfReference extends ActiveRecordModelInsta
342366class ActiveRecordInstance extends DataFlow:: Node {
343367 private ActiveRecordModelInstantiation instantiation ;
344368
345- ActiveRecordInstance ( ) { this = instantiation or instantiation . flowsTo ( this ) }
369+ ActiveRecordInstance ( ) { this = instantiation . track ( ) . getAValueReachableFromSource ( ) }
346370
347371 /** Gets the `ActiveRecordModelClass` that this is an instance of. */
348372 ActiveRecordModelClass getClass ( ) { result = instantiation .getClass ( ) }
@@ -380,12 +404,12 @@ private module Persistence {
380404 /** A call to e.g. `User.create(name: "foo")` */
381405 private class CreateLikeCall extends DataFlow:: CallNode , PersistentWriteAccess:: Range {
382406 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+ ] )
389413 }
390414
391415 override DataFlow:: Node getValue ( ) {
@@ -402,8 +426,7 @@ private module Persistence {
402426 /** A call to e.g. `User.update(1, name: "foo")` */
403427 private class UpdateLikeClassMethodCall extends DataFlow:: CallNode , PersistentWriteAccess:: Range {
404428 UpdateLikeClassMethodCall ( ) {
405- exists ( this .asExpr ( ) .getExpr ( ) .( ActiveRecordModelClassMethodCall ) .getReceiverClass ( ) ) and
406- this .getMethodName ( ) = [ "update" , "update!" , "upsert" ]
429+ this = activeRecordBaseClass ( ) .getAMethodCall ( [ "update" , "update!" , "upsert" ] )
407430 }
408431
409432 override DataFlow:: Node getValue ( ) {
@@ -448,10 +471,7 @@ private module Persistence {
448471 * ```
449472 */
450473 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" ) }
455475
456476 override DataFlow:: Node getValue ( ) { result = this .getKeywordArgument ( "time" ) }
457477 }
@@ -461,8 +481,7 @@ private module Persistence {
461481 private ExprNodes:: ArrayLiteralCfgNode arr ;
462482
463483 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
466485 arr = this .getArgument ( 0 ) .asExpr ( )
467486 }
468487
0 commit comments