@@ -314,3 +314,183 @@ private class ActiveRecordInstanceMethodCall extends DataFlow::CallNode {
314
314
315
315
ActiveRecordInstance getInstance ( ) { result = instance }
316
316
}
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