@@ -316,3 +316,151 @@ private class ActiveRecordInstanceMethodCall extends DataFlow::CallNode {
316316
317317 ActiveRecordInstance getInstance ( ) { result = instance }
318318}
319+
320+ /**
321+ * Provides modeling relating to the `ActiveRecord::Persistence` module.
322+ */
323+ private module Persistence {
324+ /**
325+ * Holds if there is a hash literal argument to `call` at `argIndex`
326+ * containing a KV pair with value `value`.
327+ */
328+ private predicate hashArgumentWithValue (
329+ DataFlow:: CallNode call , int argIndex , DataFlow:: ExprNode value
330+ ) {
331+ exists ( ExprNodes:: HashLiteralCfgNode hash , ExprNodes:: PairCfgNode pair |
332+ hash = call .getArgument ( argIndex ) .asExpr ( ) and
333+ pair = hash .getAKeyValuePair ( )
334+ |
335+ value .asExpr ( ) = pair .getValue ( )
336+ )
337+ }
338+
339+ /**
340+ * Holds if `call` has a keyword argument of with value `value`.
341+ */
342+ private predicate keywordArgumentWithValue ( DataFlow:: CallNode call , DataFlow:: ExprNode value ) {
343+ exists ( ExprNodes:: PairCfgNode pair | pair = call .getArgument ( _) .asExpr ( ) |
344+ value .asExpr ( ) = pair .getValue ( )
345+ )
346+ }
347+
348+ /** A call to e.g. `User.create(name: "foo")` */
349+ private class CreateLikeCall extends DataFlow:: CallNode , PersistentWriteAccess:: Range {
350+ private ActiveRecordModelClass modelCls ;
351+
352+ CreateLikeCall ( ) {
353+ modelCls = this .asExpr ( ) .getExpr ( ) .( ActiveRecordModelClassMethodCall ) .getReceiverClass ( ) and
354+ this .getMethodName ( ) =
355+ [
356+ "create" , "create!" , "create_or_find_by" , "create_or_find_by!" , "find_or_create_by" ,
357+ "find_or_create_by!" , "insert" , "insert!"
358+ ]
359+ }
360+
361+ override DataFlow:: Node getValue ( ) {
362+ // attrs as hash elements in arg0
363+ hashArgumentWithValue ( this , 0 , result ) or
364+ keywordArgumentWithValue ( this , result )
365+ }
366+ }
367+
368+ /** A call to e.g. `User.update(1, name: "foo")` */
369+ private class UpdateLikeClassMethodCall extends DataFlow:: CallNode , PersistentWriteAccess:: Range {
370+ private ActiveRecordModelClass modelCls ;
371+
372+ UpdateLikeClassMethodCall ( ) {
373+ modelCls = this .asExpr ( ) .getExpr ( ) .( ActiveRecordModelClassMethodCall ) .getReceiverClass ( ) and
374+ this .getMethodName ( ) = [ "update" , "update!" , "upsert" ]
375+ }
376+
377+ override DataFlow:: Node getValue ( ) {
378+ keywordArgumentWithValue ( this , result )
379+ or
380+ // Case where 2 array args are passed - the first an array of IDs, and the
381+ // second an array of hashes - each hash corresponding to an ID in the
382+ // first array.
383+ exists ( ExprNodes:: ArrayLiteralCfgNode hashesArray |
384+ this .getArgument ( 0 ) .asExpr ( ) instanceof ExprNodes:: ArrayLiteralCfgNode and
385+ hashesArray = this .getArgument ( 1 ) .asExpr ( )
386+ |
387+ exists ( ExprNodes:: HashLiteralCfgNode hash , ExprNodes:: PairCfgNode pair |
388+ hash = hashesArray .getArgument ( _) and
389+ pair = hash .getAKeyValuePair ( )
390+ |
391+ result .asExpr ( ) = pair .getValue ( )
392+ )
393+ )
394+ }
395+ }
396+
397+ /** A call to e.g. `User.insert_all([{name: "foo"}, {name: "bar"}])` */
398+ private class InsertAllLikeCall extends DataFlow:: CallNode , PersistentWriteAccess:: Range {
399+ private ExprNodes:: ArrayLiteralCfgNode arr ;
400+ private ActiveRecordModelClass modelCls ;
401+
402+ InsertAllLikeCall ( ) {
403+ modelCls = this .asExpr ( ) .getExpr ( ) .( ActiveRecordModelClassMethodCall ) .getReceiverClass ( ) and
404+ this .getMethodName ( ) = [ "insert_all" , "insert_all!" , "upsert_all" ] and
405+ arr = this .getArgument ( 0 ) .asExpr ( )
406+ }
407+
408+ override DataFlow:: Node getValue ( ) {
409+ // attrs as hash elements of members of array arg0
410+ exists ( ExprNodes:: HashLiteralCfgNode hash , ExprNodes:: PairCfgNode pair |
411+ hash = arr .getArgument ( _) and
412+ pair = hash .getAKeyValuePair ( )
413+ |
414+ result .asExpr ( ) = pair .getValue ( )
415+ )
416+ }
417+ }
418+
419+ /** A call to e.g. `user.update(name: "foo")` */
420+ private class UpdateLikeInstanceMethodCall extends PersistentWriteAccess:: Range ,
421+ ActiveRecordInstanceMethodCall {
422+ UpdateLikeInstanceMethodCall ( ) {
423+ this .getMethodName ( ) = [ "update" , "update!" , "update_attributes" , "update_attributes!" ]
424+ }
425+
426+ override DataFlow:: Node getValue ( ) {
427+ // attrs as hash elements in arg0
428+ hashArgumentWithValue ( this , 0 , result )
429+ or
430+ // keyword arg
431+ keywordArgumentWithValue ( this , result )
432+ }
433+ }
434+
435+ /** A call to e.g. `user.update_attribute(name, "foo")` */
436+ private class UpdateAttributeCall extends PersistentWriteAccess:: Range ,
437+ ActiveRecordInstanceMethodCall {
438+ UpdateAttributeCall ( ) { this .getMethodName ( ) = "update_attribute" }
439+
440+ override DataFlow:: Node getValue ( ) {
441+ // e.g. `foo.update_attribute(key, value)`
442+ result = this .getArgument ( 1 )
443+ }
444+ }
445+
446+ /**
447+ * An assignment like `user.name = "foo"`. Though this does not write to the
448+ * database without a subsequent call to persist the object, it is considered
449+ * as an `PersistentWriteAccess` to avoid missing cases where the path to a
450+ * subsequent write is not clear.
451+ */
452+ private class AssignAttribute extends PersistentWriteAccess:: Range {
453+ private ExprNodes:: AssignExprCfgNode assignNode ;
454+
455+ AssignAttribute ( ) {
456+ exists ( DataFlow:: CallNode setter |
457+ assignNode = this .asExpr ( ) and
458+ setter .getArgument ( 0 ) = this and
459+ setter instanceof ActiveRecordInstanceMethodCall and
460+ setter .asExpr ( ) .getExpr ( ) instanceof SetterMethodCall
461+ )
462+ }
463+
464+ override DataFlow:: Node getValue ( ) { assignNode .getRhs ( ) = result .asExpr ( ) }
465+ }
466+ }
0 commit comments