@@ -2,7 +2,10 @@ private import codeql.ruby.AST
22private import codeql.ruby.Concepts
33private import codeql.ruby.controlflow.CfgNodes
44private import codeql.ruby.DataFlow
5+ private import codeql.ruby.dataflow.internal.DataFlowDispatch
56private import codeql.ruby.ast.internal.Module
7+ private import codeql.ruby.ApiGraphs
8+ private import codeql.ruby.frameworks.StandardLibrary
69
710private class ActiveRecordBaseAccess extends ConstantReadAccess {
811 ActiveRecordBaseAccess ( ) {
@@ -160,5 +163,138 @@ class ActiveRecordSqlExecutionRange extends SqlExecution::Range {
160163
161164 override DataFlow:: Node getSql ( ) { result = this }
162165}
166+
163167// TODO: model `ActiveRecord` sanitizers
164168// https://api.rubyonrails.org/classes/ActiveRecord/Sanitization/ClassMethods.html
169+ // TODO: factor this out
170+ private string constantQualifiedName ( ConstantWriteAccess w ) {
171+ /* get the qualified name for the parent module, then append w */
172+ exists ( ConstantWriteAccess parent | parent = w .getEnclosingModule ( ) |
173+ result = constantQualifiedName ( parent ) + "::" + w .getName ( )
174+ )
175+ or
176+ /* base case - there's no parent module */
177+ not exists ( ConstantWriteAccess parent | parent = w .getEnclosingModule ( ) ) and
178+ result = w .getName ( )
179+ }
180+
181+ /**
182+ * A node that may evaluate to one or more `ActiveRecordModelClass` instances.
183+ */
184+ abstract class ActiveRecordModelInstantiation extends DataFlow:: Node {
185+ abstract ActiveRecordModelClass getClass ( ) ;
186+ }
187+
188+ // Names of class methods on ActiveRecord models that may return one or more
189+ // instance of that model. This also includes the `initialize` method.
190+ // See https://api.rubyonrails.org/classes/ActiveRecord/FinderMethods.html
191+ private string finderMethodName ( ) {
192+ exists ( string baseName |
193+ baseName =
194+ [
195+ "fifth" , "find" , "find_by" , "find_or_initialize_by" , "find_or_create_by" , "first" ,
196+ "forty_two" , "fourth" , "last" , "second" , "second_to_last" , "take" , "third" , "third_to_last"
197+ ] and
198+ ( result = baseName or result = baseName + "!" )
199+ )
200+ or
201+ result = "new"
202+ }
203+
204+ // Gets the "final" receiver in a chain of method calls.
205+ // For example, in `Foo.bar`, this would give the `Foo` access, and in
206+ // `foo.bar.baz("arg")` it would give the `foo` variable access
207+ private Expr getUltimateReceiver ( MethodCall call ) {
208+ exists ( Expr recv |
209+ recv = call .getReceiver ( ) and
210+ (
211+ result = getUltimateReceiver ( recv )
212+ or
213+ not recv instanceof MethodCall and result = recv
214+ )
215+ )
216+ }
217+
218+ // A call to `find`, `where`, etc. that may return active record model object(s)
219+ private class ActiveRecordModelFinderCall extends ActiveRecordModelInstantiation , DataFlow:: CallNode {
220+ private MethodCall call ;
221+ private ActiveRecordModelClass cls ;
222+ private Expr recv ;
223+
224+ ActiveRecordModelFinderCall ( ) {
225+ call = this .asExpr ( ) .getExpr ( ) and
226+ recv = getUltimateReceiver ( call ) and
227+ resolveConstant ( recv ) = constantQualifiedName ( cls ) and
228+ call .getMethodName ( ) = finderMethodName ( )
229+ }
230+
231+ final override ActiveRecordModelClass getClass ( ) { result = cls }
232+
233+ string getConstantQualifiedClassName ( ) { result = constantQualifiedName ( cls ) }
234+
235+ Expr getUltimateReceiver ( ) { result = recv }
236+ }
237+
238+ // A `self` reference that may resolve to an active record model object
239+ private class ActiveRecordModelClassSelfReference extends ActiveRecordModelInstantiation ,
240+ DataFlow:: LocalSourceNode {
241+ private ActiveRecordModelClass cls ;
242+
243+ ActiveRecordModelClassSelfReference ( ) {
244+ exists ( Self s |
245+ s .getEnclosingModule ( ) = cls and
246+ s .getEnclosingMethod ( ) = cls .getAMethod ( ) and
247+ s = this .asExpr ( ) .getExpr ( )
248+ )
249+ }
250+
251+ final override ActiveRecordModelClass getClass ( ) { result = cls }
252+ }
253+
254+ // A (locally tracked) active record model object
255+ private DataFlow:: Node activeRecordModelInstance ( ) {
256+ result instanceof ActiveRecordModelInstantiation
257+ or
258+ exists ( ActiveRecordModelInstantiation inst | inst .( DataFlow:: LocalSourceNode ) .flowsTo ( result ) )
259+ }
260+
261+ // A call whose receiver may be an active record model object
262+ private class ActiveRecordInstanceMethodCall extends DataFlow:: CallNode {
263+ ActiveRecordInstanceMethodCall ( ) { this .getReceiver ( ) = activeRecordModelInstance ( ) }
264+ }
265+
266+ private string activeRecordPersistenceInstanceMethodName ( ) {
267+ result =
268+ [
269+ "becomes" , "becomes!" , "decrement" , "decrement!" , "delete" , "delete!" , "destroy" , "destroy!" ,
270+ "destroyed?" , "increment" , "increment!" , "new_record?" , "persisted?" ,
271+ "previously_new_record?" , "reload" , "save" , "save!" , "toggle" , "toggle!" , "touch" , "update" ,
272+ "update!" , "update_attribute" , "update_column" , "update_columns"
273+ ]
274+ }
275+
276+ private predicate isCallToBuiltInMethod ( MethodCall c ) {
277+ c .getMethodName ( ) = activeRecordPersistenceInstanceMethodName ( ) or
278+ c instanceof BasicObjectInstanceMethodCall or
279+ c instanceof ObjectInstanceMethodCall
280+ }
281+
282+ /**
283+ * Returns true if `call` may refer to a method that returns a database value
284+ * if invoked against a `sourceClass` instance.
285+ */
286+ predicate activeRecordMethodMayAccessField ( ActiveRecordModelClass sourceClass , MethodCall call ) {
287+ not (
288+ // Methods whose names can be hardcoded
289+ isCallToBuiltInMethod ( call )
290+ or
291+ // Methods defined in `sourceClass` that do not return database fields
292+ exists ( Method m | m = sourceClass .getMethod ( call .getMethodName ( ) ) |
293+ forall ( DataFlow:: Node returned , ActiveRecordInstanceMethodCall c |
294+ exprNodeReturnedFrom ( returned , m ) and c .flowsTo ( returned )
295+ |
296+ not activeRecordMethodMayAccessField ( sourceClass , returned .asExpr ( ) .getExpr ( ) )
297+ )
298+ )
299+ )
300+ }
0 commit comments