Skip to content

Commit 63ef9a7

Browse files
committed
Ruby: model OrmWriteAccesses for ActiveRecord
1 parent b1fd321 commit 63ef9a7

File tree

1 file changed

+180
-0
lines changed

1 file changed

+180
-0
lines changed

ruby/ql/lib/codeql/ruby/frameworks/ActiveRecord.qll

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)