@@ -314,3 +314,183 @@ private class ActiveRecordInstanceMethodCall extends DataFlow::CallNode {
314314
315315 ActiveRecordInstance getInstance ( ) { result = instance }
316316}
317+
318+ /**
319+ * Provides modeling relating to the `ActiveRecord::Persistence` module.
320+ */
321+ private module Persistence {
322+ /**
323+ * A call to a method that may modify or create a model object and write it to
324+ * the database. Examples include `create`, `insert`, and `update`.
325+ */
326+ abstract class ModifyAndSaveCall extends DataFlow:: CallNode , OrmWriteAccess:: Range {
327+ /**
328+ * Holds if the given key-value pair is set on an object by this call.
329+ */
330+ abstract predicate setsKeyValuePair ( ExprCfgNode key , ExprCfgNode value ) ;
331+
332+ /**
333+ * Gets the ActiveRecord model class to which this call applies.
334+ */
335+ abstract ActiveRecordModelClass getClass ( ) ;
336+
337+ final override string getFieldNameAssignedTo ( DataFlow:: Node value ) {
338+ exists ( ExprCfgNode keyExpr , ExprCfgNode valueExpr |
339+ this .setsKeyValuePair ( keyExpr , valueExpr )
340+ |
341+ keyExpr .getConstantValue ( ) .isStringOrSymbol ( result ) and
342+ // avoid vacuous matches where the key is not a string or not a symbol
343+ not result = "" and
344+ value .asExpr ( ) = valueExpr
345+ )
346+ }
347+ }
348+
349+ /**
350+ * Holds if there is a hash literal argument to `call` at `argIndex`
351+ * containing a `key`-`value` pair.
352+ */
353+ private predicate hashArgument (
354+ DataFlow:: CallNode call , int argIndex , ExprCfgNode key , ExprCfgNode value
355+ ) {
356+ exists ( ExprNodes:: HashLiteralCfgNode hash , ExprNodes:: PairCfgNode pair |
357+ hash = call .getArgument ( argIndex ) .asExpr ( ) and
358+ pair = hash .getAKeyValuePair ( )
359+ |
360+ key = pair .getKey ( ) and value = pair .getValue ( )
361+ )
362+ }
363+
364+ /**
365+ * Holds if `call` has a keyword argument of the form `key: value`.
366+ */
367+ private predicate keywordArgument ( DataFlow:: CallNode call , ExprCfgNode key , ExprCfgNode value ) {
368+ exists ( ExprNodes:: PairCfgNode pair | pair = call .getArgument ( _) .asExpr ( ) |
369+ key = pair .getKey ( ) and value = pair .getValue ( )
370+ )
371+ }
372+
373+ /** A call to e.g. `User.create(name: "foo")` */
374+ private class CreateLikeCall extends ModifyAndSaveCall {
375+ private ActiveRecordModelClass modelCls ;
376+
377+ CreateLikeCall ( ) {
378+ modelCls = this .asExpr ( ) .getExpr ( ) .( ActiveRecordModelClassMethodCall ) .getReceiverClass ( ) and
379+ this .getMethodName ( ) =
380+ [
381+ "create" , "create!" , "create_or_find_by" , "create_or_find_by!" , "find_or_create_by" ,
382+ "find_or_create_by!" , "insert" , "insert!"
383+ ]
384+ }
385+
386+ override predicate setsKeyValuePair ( ExprCfgNode key , ExprCfgNode value ) {
387+ // attrs as hash elements in arg0
388+ hashArgument ( this , 0 , key , value ) or
389+ keywordArgument ( this , key , value )
390+ }
391+
392+ override ActiveRecordModelClass getClass ( ) { result = modelCls }
393+ }
394+
395+ /** A call to e.g. `User.update(1, name: "foo")` */
396+ private class UpdateLikeClassMethodCall extends ModifyAndSaveCall {
397+ private ActiveRecordModelClass modelCls ;
398+
399+ UpdateLikeClassMethodCall ( ) {
400+ modelCls = this .asExpr ( ) .getExpr ( ) .( ActiveRecordModelClassMethodCall ) .getReceiverClass ( ) and
401+ this .getMethodName ( ) = [ "update" , "update!" , "upsert" ]
402+ }
403+
404+ override predicate setsKeyValuePair ( ExprCfgNode key , ExprCfgNode value ) {
405+ keywordArgument ( this , key , value )
406+ or
407+ // Case where 2 array args are passed - the first an array of IDs, and the
408+ // second an array of hashes - each hash corresponding to an ID in the
409+ // first array.
410+ exists ( ExprNodes:: ArrayLiteralCfgNode hashesArray |
411+ this .getArgument ( 0 ) .asExpr ( ) instanceof ExprNodes:: ArrayLiteralCfgNode and
412+ hashesArray = this .getArgument ( 1 ) .asExpr ( )
413+ |
414+ exists ( ExprNodes:: HashLiteralCfgNode hash , ExprNodes:: PairCfgNode pair |
415+ hash = hashesArray .getArgument ( _) and
416+ pair = hash .getAKeyValuePair ( )
417+ |
418+ key = pair .getKey ( ) and value = pair .getValue ( )
419+ )
420+ )
421+ }
422+
423+ override ActiveRecordModelClass getClass ( ) { result = modelCls }
424+ }
425+
426+ /** A call to e.g. `User.insert_all([{name: "foo"}, {name: "bar"}])` */
427+ private class InsertAllLikeCall extends ModifyAndSaveCall {
428+ private ExprNodes:: ArrayLiteralCfgNode arr ;
429+ private ActiveRecordModelClass modelCls ;
430+
431+ InsertAllLikeCall ( ) {
432+ modelCls = this .asExpr ( ) .getExpr ( ) .( ActiveRecordModelClassMethodCall ) .getReceiverClass ( ) and
433+ this .getMethodName ( ) = [ "insert_all" , "insert_all!" , "upsert_all" ] and
434+ arr = this .getArgument ( 0 ) .asExpr ( )
435+ }
436+
437+ override predicate setsKeyValuePair ( ExprCfgNode key , ExprCfgNode value ) {
438+ // attrs as hash elements of members of array arg0
439+ exists ( ExprNodes:: HashLiteralCfgNode hash , ExprNodes:: PairCfgNode pair |
440+ hash = arr .getArgument ( _) and
441+ pair = hash .getAKeyValuePair ( )
442+ |
443+ key = pair .getKey ( ) and value = pair .getValue ( )
444+ )
445+ }
446+
447+ override ActiveRecordModelClass getClass ( ) { result = modelCls }
448+ }
449+
450+ /** A call to e.g. `user.update(name: "foo")` */
451+ private class UpdateLikeInstanceMethodCall extends ModifyAndSaveCall ,
452+ ActiveRecordInstanceMethodCall {
453+ UpdateLikeInstanceMethodCall ( ) {
454+ this .getMethodName ( ) = [ "update" , "update!" , "update_attributes" , "update_attributes!" ]
455+ }
456+
457+ override predicate setsKeyValuePair ( ExprCfgNode key , ExprCfgNode value ) {
458+ // attrs as hash elements in arg0
459+ hashArgument ( this , 0 , key , value )
460+ or
461+ // keyword arg
462+ keywordArgument ( this , key , value )
463+ }
464+
465+ override ActiveRecordModelClass getClass ( ) { result = this .getInstance ( ) .getClass ( ) }
466+ }
467+
468+ /** A call to e.g. `user.update_attribute(name, "foo")` */
469+ private class UpdateAttributeCall extends ModifyAndSaveCall , ActiveRecordInstanceMethodCall {
470+ UpdateAttributeCall ( ) { this .getMethodName ( ) = "update_attribute" }
471+
472+ override predicate setsKeyValuePair ( ExprCfgNode key , ExprCfgNode value ) {
473+ // e.g. `foo.update_attribute(key, value)`
474+ key = this .getArgument ( 0 ) .asExpr ( ) and value = this .getArgument ( 1 ) .asExpr ( )
475+ }
476+
477+ override ActiveRecordModelClass getClass ( ) { result = this .getInstance ( ) .getClass ( ) }
478+ }
479+
480+ /**
481+ * An assignment like `user.name = "foo"`. Though this does not write to the
482+ * database without a subsequent call to persist the object, it is considered
483+ * as an `OrmWriteAccess` to avoid missing cases where the path to a
484+ * subsequent write is not clear.
485+ */
486+ private class AssignAttributeCall extends DataFlow:: CallNode , ActiveRecordInstanceMethodCall ,
487+ OrmWriteAccess:: Range {
488+ AssignAttributeCall ( ) { this .asExpr ( ) .getExpr ( ) instanceof SetterMethodCall }
489+
490+ override string getFieldNameAssignedTo ( DataFlow:: Node value ) {
491+ result + "=" = this .getMethodName ( ) and
492+ // match RHS
493+ this .getArgument ( 0 ) .asExpr ( ) .( ExprNodes:: AssignExprCfgNode ) .getRhs ( ) = value .asExpr ( )
494+ }
495+ }
496+ }
0 commit comments