Ruby: Model ActiveResource

This commit is contained in:
Harry Maclean
2022-08-05 15:37:19 +12:00
parent 09ad1c29bd
commit aa6edb0edb
4 changed files with 372 additions and 0 deletions

View File

@@ -0,0 +1,274 @@
/**
* Provides modeling for the `ActiveResource` library.
* Version: 6.0.0.
*/
private import ruby
private import codeql.ruby.Concepts
private import codeql.ruby.controlflow.CfgNodes
private import codeql.ruby.ast.internal.Module
private import codeql.ruby.DataFlow
private import codeql.ruby.ApiGraphs
/**
* Provides modeling for the `ActiveResource` library.
* Version: 6.0.0.
*/
module ActiveResource {
/**
* An ActiveResource model class. This is any (transitive) subclass of ActiveResource.
*/
private API::Node modelApiNode() {
result = API::getTopLevelMember("ActiveResource").getMember("Base").getASubclass+()
}
/**
* An ActiveResource class.
*
* ``rb
* class Person < ActiveResource::Base
* end
* ```
*/
class ModelClass extends ClassDeclaration {
API::Node model;
ModelClass() {
model = modelApiNode() and
this.getSuperclassExpr() = model.getAValueReachableFromSource().asExpr().getExpr()
}
API::Node getModelApiNode() { result = model }
SiteAssignCall getASiteAssignment() { result.getModelClass() = this }
predicate disablesCertificateValidation(SiteAssignCall c) {
c = this.getASiteAssignment() and
c.disablesCertificateValidation()
}
}
/**
* A call to a class method on an ActiveResource model class.
*
* ```rb
* class Person < ActiveResource::Base
* end
*
* Person.find(1)
* ```
*/
class ModelClassMethodCall extends DataFlow::CallNode {
API::Node model;
ModelClassMethodCall() {
model = modelApiNode() and
this = classMethodCall(model, _)
}
ModelClass getModelClass() { result.getModelApiNode() = model }
}
/**
* A call to `site=` on an ActiveResource model class.
* This sets the base URL for all HTTP requests made by this class.
*/
private class SiteAssignCall extends DataFlow::CallNode {
API::Node model;
SiteAssignCall() { model = modelApiNode() and this = classMethodCall(model, "site=") }
/**
* A node that contributes to the URLs used for HTTP requests by the parent
* class.
*/
DataFlow::Node getAUrlPart() { result = this.getArgument(0) }
ModelClass getModelClass() { result.getModelApiNode() = model }
predicate disablesCertificateValidation() {
this.getAUrlPart().asExpr().getConstantValue().getString().regexpMatch("^http(^s)")
}
}
/**
* A call to the `find` class method, which returns an ActiveResource model
* object.
*
* ```rb
* alice = Person.find(1)
* ```
*/
class FindCall extends ModelClassMethodCall {
FindCall() { this.getMethodName() = "find" }
}
/**
* A call to the `create(!)` class method, which returns an ActiveResource model
* object.
*
* ```rb
* alice = Person.create(name: "alice")
* ```
*/
class CreateCall extends ModelClassMethodCall {
CreateCall() { this.getMethodName() = ["create", "create!"] }
}
/**
* A call to a method that sends a custom HTTP request outside of the
* ActiveResource conventions. This typically returns an ActiveResource model
* object. It may return a collection, but we don't currently model that.
*
* ```rb
* alice = Person.get(:active)
* ```
*/
class CustomHttpCall extends ModelClassMethodCall {
CustomHttpCall() { this.getMethodName() = ["get", "post", "put", "patch", "delete"] }
}
/**
* An ActiveResource model object.
*/
class ModelInstance extends DataFlow::Node {
ModelClass cls;
ModelInstance() {
exists(API::Node model | model = modelApiNode() |
this = model.getInstance().getAValueReachableFromSource() and
cls.getModelApiNode() = model
)
or
exists(FindCall call | call.flowsTo(this) | cls = call.getModelClass())
or
exists(CreateCall call | call.flowsTo(this) | cls = call.getModelClass())
or
exists(CustomHttpCall call | call.flowsTo(this) | cls = call.getModelClass())
or
exists(CollectionCall call |
call.getMethodName() = ["first", "last"] and
call.flowsTo(this)
|
cls = call.getCollection().getModelClass()
)
}
ModelClass getModelClass() { result = cls }
}
/**
* A call to a method on an ActiveResource model object.
*/
class ModelInstanceMethodCall extends DataFlow::CallNode {
ModelInstance i;
ModelInstanceMethodCall() { this.getReceiver() = i }
ModelInstance getInstance() { result = i }
ModelClass getModelClass() { result = this.getReceiver().(ModelInstance).getModelClass() }
}
/**
*A collection of ActiveResource model objects.
*/
class Collection extends DataFlow::Node {
ModelClassMethodCall classMethodCall;
Collection() {
exists(ModelClassMethodCall c | c.flowsTo(this) |
c.getMethodName() = "all"
or
c.getMethodName() = "find" and
c.getArgument(0).asExpr().getConstantValue().isStringlikeValue("all")
)
}
ModelClass getModelClass() { result = classMethodCall.getModelClass() }
}
/**
* A method call on a collection.
*/
class CollectionCall extends DataFlow::CallNode {
CollectionCall() { this.getReceiver() instanceof Collection }
Collection getCollection() { result = this.getReceiver() }
}
private class ModelClassMethodCallAsHttpRequest extends HTTP::Client::Request::Range {
ModelClassMethodCall call;
ModelClass cls;
ModelClassMethodCallAsHttpRequest() {
this = call.asExpr().getExpr() and
call.getModelClass() = cls and
call.getMethodName() = ["all", "build", "create", "create!", "find", "first", "last"]
}
override string getFramework() { result = "ActiveResource" }
override predicate disablesCertificateValidation(DataFlow::Node disablingNode) {
cls.disablesCertificateValidation(disablingNode)
}
override DataFlow::Node getAUrlPart() { result = cls.getASiteAssignment().getAUrlPart() }
override DataFlow::Node getResponseBody() { result = call }
}
private class ModelInstanceMethodCallAsHttpRequest extends HTTP::Client::Request::Range {
ModelInstanceMethodCall call;
ModelClass cls;
ModelInstanceMethodCallAsHttpRequest() {
this = call.asExpr().getExpr() and
call.getModelClass() = cls and
call.getMethodName() =
[
"exists?", "reload", "save", "save!", "destroy", "delete", "get", "patch", "post", "put",
"update_attribute", "update_attributes"
]
}
override string getFramework() { result = "ActiveResource" }
override predicate disablesCertificateValidation(DataFlow::Node disablingNode) {
cls.disablesCertificateValidation(disablingNode)
}
override DataFlow::Node getAUrlPart() { result = cls.getASiteAssignment().getAUrlPart() }
override DataFlow::Node getResponseBody() { result = call }
}
/**
* A call to a class method.
*
* TODO: is this general enough to be useful elsewhere?
*
* Examples:
* ```rb
* class A
* def self.m; end
*
* m # call
* end
*
* A.m # call
* ```
*/
private DataFlow::CallNode classMethodCall(API::Node classNode, string methodName) {
// A.m
result = classNode.getAMethodCall(methodName)
or
// class A
// A.m
// end
result.getReceiver().asExpr() instanceof ExprNodes::SelfVariableAccessCfgNode and
result.asExpr().getExpr().getEnclosingModule().(ClassDeclaration).getSuperclassExpr() =
classNode.getAValueReachableFromSource().asExpr().getExpr() and
result.getMethodName() = methodName
}
}

View File

@@ -0,0 +1,52 @@
modelClasses
| active_resource.rb:1:1:3:3 | Person | active_resource.rb:2:3:2:11 | call to site= |
| active_resource.rb:29:1:31:3 | Post | active_resource.rb:30:3:30:11 | call to site= |
modelClassMethodCalls
| active_resource.rb:2:3:2:11 | call to site= |
| active_resource.rb:5:9:5:33 | call to new |
| active_resource.rb:8:9:8:22 | call to find |
| active_resource.rb:16:1:16:23 | call to new |
| active_resource.rb:18:1:18:22 | call to get |
| active_resource.rb:23:10:23:19 | call to all |
| active_resource.rb:24:10:24:26 | call to find |
| active_resource.rb:30:3:30:11 | call to site= |
modelInstances
| active_resource.rb:5:1:5:33 | ... = ... |
| active_resource.rb:5:1:5:33 | ... = ... |
| active_resource.rb:5:9:5:33 | call to new |
| active_resource.rb:6:1:6:5 | alice |
| active_resource.rb:8:1:8:22 | ... = ... |
| active_resource.rb:8:1:8:22 | ... = ... |
| active_resource.rb:8:9:8:22 | call to find |
| active_resource.rb:9:1:9:5 | alice |
| active_resource.rb:10:1:10:5 | alice |
| active_resource.rb:12:1:12:5 | alice |
| active_resource.rb:16:1:16:23 | call to new |
| active_resource.rb:17:1:17:5 | alice |
| active_resource.rb:18:1:18:22 | call to get |
| active_resource.rb:19:1:19:5 | alice |
| active_resource.rb:24:1:24:26 | ... = ... |
| active_resource.rb:24:1:24:26 | ... = ... |
| active_resource.rb:24:10:24:26 | call to find |
| active_resource.rb:26:1:26:20 | ... = ... |
| active_resource.rb:26:1:26:20 | ... = ... |
| active_resource.rb:26:9:26:14 | people |
| active_resource.rb:26:9:26:20 | call to first |
| active_resource.rb:27:1:27:5 | alice |
modelInstanceMethodCalls
| active_resource.rb:6:1:6:10 | call to save |
| active_resource.rb:9:1:9:13 | call to address= |
| active_resource.rb:10:1:10:10 | call to save |
| active_resource.rb:12:1:12:13 | call to destroy |
| active_resource.rb:16:1:16:39 | call to post |
| active_resource.rb:17:1:17:19 | call to put |
| active_resource.rb:19:1:19:19 | call to delete |
| active_resource.rb:26:9:26:20 | call to first |
| active_resource.rb:27:1:27:10 | call to save |
collections
| active_resource.rb:23:1:23:19 | ... = ... |
| active_resource.rb:23:10:23:19 | call to all |
| active_resource.rb:24:1:24:26 | ... = ... |
| active_resource.rb:24:1:24:26 | ... = ... |
| active_resource.rb:24:10:24:26 | call to find |
| active_resource.rb:26:9:26:14 | people |

View File

@@ -0,0 +1,15 @@
import ruby
import codeql.ruby.DataFlow
import codeql.ruby.frameworks.ActiveResource
query predicate modelClasses(ActiveResource::ModelClass c, DataFlow::Node siteAssignCall) {
c.getASiteAssignment() = siteAssignCall
}
query predicate modelClassMethodCalls(ActiveResource::ModelClassMethodCall c) { any() }
query predicate modelInstances(ActiveResource::ModelInstance c) { any() }
query predicate modelInstanceMethodCalls(ActiveResource::ModelInstanceMethodCall c) { any() }
query predicate collections(ActiveResource::Collection c) { any() }

View File

@@ -0,0 +1,31 @@
class Person < ActiveResource::Base
self.site = "https://api.example.com"
end
alice = Person.new(name: "Alice")
alice.save
alice = Person.find(1)
alice.address = "123 Main Street"
alice.save
alice.destroy
# Custom REST methods
Person.new(name: "Bob").post(:register)
alice.put(:promote)
Person.get(:positions)
alice.delete(:fire)
# Collections
people = Person.all
people = Person.find(:all)
alice = people.first
alice.save
class Post < ActiveResource::Base
self.site = "http://api.insecure.com"
end