Skip to content

Commit aa6edb0

Browse files
committed
Ruby: Model ActiveResource
1 parent 09ad1c2 commit aa6edb0

File tree

4 files changed

+372
-0
lines changed

4 files changed

+372
-0
lines changed
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
/**
2+
* Provides modeling for the `ActiveResource` library.
3+
* Version: 6.0.0.
4+
*/
5+
6+
private import ruby
7+
private import codeql.ruby.Concepts
8+
private import codeql.ruby.controlflow.CfgNodes
9+
private import codeql.ruby.ast.internal.Module
10+
private import codeql.ruby.DataFlow
11+
private import codeql.ruby.ApiGraphs
12+
13+
/**
14+
* Provides modeling for the `ActiveResource` library.
15+
* Version: 6.0.0.
16+
*/
17+
module ActiveResource {
18+
/**
19+
* An ActiveResource model class. This is any (transitive) subclass of ActiveResource.
20+
*/
21+
private API::Node modelApiNode() {
22+
result = API::getTopLevelMember("ActiveResource").getMember("Base").getASubclass+()
23+
}
24+
25+
/**
26+
* An ActiveResource class.
27+
*
28+
* ``rb
29+
* class Person < ActiveResource::Base
30+
* end
31+
* ```
32+
*/
33+
class ModelClass extends ClassDeclaration {
34+
API::Node model;
35+
36+
ModelClass() {
37+
model = modelApiNode() and
38+
this.getSuperclassExpr() = model.getAValueReachableFromSource().asExpr().getExpr()
39+
}
40+
41+
API::Node getModelApiNode() { result = model }
42+
43+
SiteAssignCall getASiteAssignment() { result.getModelClass() = this }
44+
45+
predicate disablesCertificateValidation(SiteAssignCall c) {
46+
c = this.getASiteAssignment() and
47+
c.disablesCertificateValidation()
48+
}
49+
}
50+
51+
/**
52+
* A call to a class method on an ActiveResource model class.
53+
*
54+
* ```rb
55+
* class Person < ActiveResource::Base
56+
* end
57+
*
58+
* Person.find(1)
59+
* ```
60+
*/
61+
class ModelClassMethodCall extends DataFlow::CallNode {
62+
API::Node model;
63+
64+
ModelClassMethodCall() {
65+
model = modelApiNode() and
66+
this = classMethodCall(model, _)
67+
}
68+
69+
ModelClass getModelClass() { result.getModelApiNode() = model }
70+
}
71+
72+
/**
73+
* A call to `site=` on an ActiveResource model class.
74+
* This sets the base URL for all HTTP requests made by this class.
75+
*/
76+
private class SiteAssignCall extends DataFlow::CallNode {
77+
API::Node model;
78+
79+
SiteAssignCall() { model = modelApiNode() and this = classMethodCall(model, "site=") }
80+
81+
/**
82+
* A node that contributes to the URLs used for HTTP requests by the parent
83+
* class.
84+
*/
85+
DataFlow::Node getAUrlPart() { result = this.getArgument(0) }
86+
87+
ModelClass getModelClass() { result.getModelApiNode() = model }
88+
89+
predicate disablesCertificateValidation() {
90+
this.getAUrlPart().asExpr().getConstantValue().getString().regexpMatch("^http(^s)")
91+
}
92+
}
93+
94+
/**
95+
* A call to the `find` class method, which returns an ActiveResource model
96+
* object.
97+
*
98+
* ```rb
99+
* alice = Person.find(1)
100+
* ```
101+
*/
102+
class FindCall extends ModelClassMethodCall {
103+
FindCall() { this.getMethodName() = "find" }
104+
}
105+
106+
/**
107+
* A call to the `create(!)` class method, which returns an ActiveResource model
108+
* object.
109+
*
110+
* ```rb
111+
* alice = Person.create(name: "alice")
112+
* ```
113+
*/
114+
class CreateCall extends ModelClassMethodCall {
115+
CreateCall() { this.getMethodName() = ["create", "create!"] }
116+
}
117+
118+
/**
119+
* A call to a method that sends a custom HTTP request outside of the
120+
* ActiveResource conventions. This typically returns an ActiveResource model
121+
* object. It may return a collection, but we don't currently model that.
122+
*
123+
* ```rb
124+
* alice = Person.get(:active)
125+
* ```
126+
*/
127+
class CustomHttpCall extends ModelClassMethodCall {
128+
CustomHttpCall() { this.getMethodName() = ["get", "post", "put", "patch", "delete"] }
129+
}
130+
131+
/**
132+
* An ActiveResource model object.
133+
*/
134+
class ModelInstance extends DataFlow::Node {
135+
ModelClass cls;
136+
137+
ModelInstance() {
138+
exists(API::Node model | model = modelApiNode() |
139+
this = model.getInstance().getAValueReachableFromSource() and
140+
cls.getModelApiNode() = model
141+
)
142+
or
143+
exists(FindCall call | call.flowsTo(this) | cls = call.getModelClass())
144+
or
145+
exists(CreateCall call | call.flowsTo(this) | cls = call.getModelClass())
146+
or
147+
exists(CustomHttpCall call | call.flowsTo(this) | cls = call.getModelClass())
148+
or
149+
exists(CollectionCall call |
150+
call.getMethodName() = ["first", "last"] and
151+
call.flowsTo(this)
152+
|
153+
cls = call.getCollection().getModelClass()
154+
)
155+
}
156+
157+
ModelClass getModelClass() { result = cls }
158+
}
159+
160+
/**
161+
* A call to a method on an ActiveResource model object.
162+
*/
163+
class ModelInstanceMethodCall extends DataFlow::CallNode {
164+
ModelInstance i;
165+
166+
ModelInstanceMethodCall() { this.getReceiver() = i }
167+
168+
ModelInstance getInstance() { result = i }
169+
170+
ModelClass getModelClass() { result = this.getReceiver().(ModelInstance).getModelClass() }
171+
}
172+
173+
/**
174+
*A collection of ActiveResource model objects.
175+
*/
176+
class Collection extends DataFlow::Node {
177+
ModelClassMethodCall classMethodCall;
178+
179+
Collection() {
180+
exists(ModelClassMethodCall c | c.flowsTo(this) |
181+
c.getMethodName() = "all"
182+
or
183+
c.getMethodName() = "find" and
184+
c.getArgument(0).asExpr().getConstantValue().isStringlikeValue("all")
185+
)
186+
}
187+
188+
ModelClass getModelClass() { result = classMethodCall.getModelClass() }
189+
}
190+
191+
/**
192+
* A method call on a collection.
193+
*/
194+
class CollectionCall extends DataFlow::CallNode {
195+
CollectionCall() { this.getReceiver() instanceof Collection }
196+
197+
Collection getCollection() { result = this.getReceiver() }
198+
}
199+
200+
private class ModelClassMethodCallAsHttpRequest extends HTTP::Client::Request::Range {
201+
ModelClassMethodCall call;
202+
ModelClass cls;
203+
204+
ModelClassMethodCallAsHttpRequest() {
205+
this = call.asExpr().getExpr() and
206+
call.getModelClass() = cls and
207+
call.getMethodName() = ["all", "build", "create", "create!", "find", "first", "last"]
208+
}
209+
210+
override string getFramework() { result = "ActiveResource" }
211+
212+
override predicate disablesCertificateValidation(DataFlow::Node disablingNode) {
213+
cls.disablesCertificateValidation(disablingNode)
214+
}
215+
216+
override DataFlow::Node getAUrlPart() { result = cls.getASiteAssignment().getAUrlPart() }
217+
218+
override DataFlow::Node getResponseBody() { result = call }
219+
}
220+
221+
private class ModelInstanceMethodCallAsHttpRequest extends HTTP::Client::Request::Range {
222+
ModelInstanceMethodCall call;
223+
ModelClass cls;
224+
225+
ModelInstanceMethodCallAsHttpRequest() {
226+
this = call.asExpr().getExpr() and
227+
call.getModelClass() = cls and
228+
call.getMethodName() =
229+
[
230+
"exists?", "reload", "save", "save!", "destroy", "delete", "get", "patch", "post", "put",
231+
"update_attribute", "update_attributes"
232+
]
233+
}
234+
235+
override string getFramework() { result = "ActiveResource" }
236+
237+
override predicate disablesCertificateValidation(DataFlow::Node disablingNode) {
238+
cls.disablesCertificateValidation(disablingNode)
239+
}
240+
241+
override DataFlow::Node getAUrlPart() { result = cls.getASiteAssignment().getAUrlPart() }
242+
243+
override DataFlow::Node getResponseBody() { result = call }
244+
}
245+
246+
/**
247+
* A call to a class method.
248+
*
249+
* TODO: is this general enough to be useful elsewhere?
250+
*
251+
* Examples:
252+
* ```rb
253+
* class A
254+
* def self.m; end
255+
*
256+
* m # call
257+
* end
258+
*
259+
* A.m # call
260+
* ```
261+
*/
262+
private DataFlow::CallNode classMethodCall(API::Node classNode, string methodName) {
263+
// A.m
264+
result = classNode.getAMethodCall(methodName)
265+
or
266+
// class A
267+
// A.m
268+
// end
269+
result.getReceiver().asExpr() instanceof ExprNodes::SelfVariableAccessCfgNode and
270+
result.asExpr().getExpr().getEnclosingModule().(ClassDeclaration).getSuperclassExpr() =
271+
classNode.getAValueReachableFromSource().asExpr().getExpr() and
272+
result.getMethodName() = methodName
273+
}
274+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
modelClasses
2+
| active_resource.rb:1:1:3:3 | Person | active_resource.rb:2:3:2:11 | call to site= |
3+
| active_resource.rb:29:1:31:3 | Post | active_resource.rb:30:3:30:11 | call to site= |
4+
modelClassMethodCalls
5+
| active_resource.rb:2:3:2:11 | call to site= |
6+
| active_resource.rb:5:9:5:33 | call to new |
7+
| active_resource.rb:8:9:8:22 | call to find |
8+
| active_resource.rb:16:1:16:23 | call to new |
9+
| active_resource.rb:18:1:18:22 | call to get |
10+
| active_resource.rb:23:10:23:19 | call to all |
11+
| active_resource.rb:24:10:24:26 | call to find |
12+
| active_resource.rb:30:3:30:11 | call to site= |
13+
modelInstances
14+
| active_resource.rb:5:1:5:33 | ... = ... |
15+
| active_resource.rb:5:1:5:33 | ... = ... |
16+
| active_resource.rb:5:9:5:33 | call to new |
17+
| active_resource.rb:6:1:6:5 | alice |
18+
| active_resource.rb:8:1:8:22 | ... = ... |
19+
| active_resource.rb:8:1:8:22 | ... = ... |
20+
| active_resource.rb:8:9:8:22 | call to find |
21+
| active_resource.rb:9:1:9:5 | alice |
22+
| active_resource.rb:10:1:10:5 | alice |
23+
| active_resource.rb:12:1:12:5 | alice |
24+
| active_resource.rb:16:1:16:23 | call to new |
25+
| active_resource.rb:17:1:17:5 | alice |
26+
| active_resource.rb:18:1:18:22 | call to get |
27+
| active_resource.rb:19:1:19:5 | alice |
28+
| active_resource.rb:24:1:24:26 | ... = ... |
29+
| active_resource.rb:24:1:24:26 | ... = ... |
30+
| active_resource.rb:24:10:24:26 | call to find |
31+
| active_resource.rb:26:1:26:20 | ... = ... |
32+
| active_resource.rb:26:1:26:20 | ... = ... |
33+
| active_resource.rb:26:9:26:14 | people |
34+
| active_resource.rb:26:9:26:20 | call to first |
35+
| active_resource.rb:27:1:27:5 | alice |
36+
modelInstanceMethodCalls
37+
| active_resource.rb:6:1:6:10 | call to save |
38+
| active_resource.rb:9:1:9:13 | call to address= |
39+
| active_resource.rb:10:1:10:10 | call to save |
40+
| active_resource.rb:12:1:12:13 | call to destroy |
41+
| active_resource.rb:16:1:16:39 | call to post |
42+
| active_resource.rb:17:1:17:19 | call to put |
43+
| active_resource.rb:19:1:19:19 | call to delete |
44+
| active_resource.rb:26:9:26:20 | call to first |
45+
| active_resource.rb:27:1:27:10 | call to save |
46+
collections
47+
| active_resource.rb:23:1:23:19 | ... = ... |
48+
| active_resource.rb:23:10:23:19 | call to all |
49+
| active_resource.rb:24:1:24:26 | ... = ... |
50+
| active_resource.rb:24:1:24:26 | ... = ... |
51+
| active_resource.rb:24:10:24:26 | call to find |
52+
| active_resource.rb:26:9:26:14 | people |
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import ruby
2+
import codeql.ruby.DataFlow
3+
import codeql.ruby.frameworks.ActiveResource
4+
5+
query predicate modelClasses(ActiveResource::ModelClass c, DataFlow::Node siteAssignCall) {
6+
c.getASiteAssignment() = siteAssignCall
7+
}
8+
9+
query predicate modelClassMethodCalls(ActiveResource::ModelClassMethodCall c) { any() }
10+
11+
query predicate modelInstances(ActiveResource::ModelInstance c) { any() }
12+
13+
query predicate modelInstanceMethodCalls(ActiveResource::ModelInstanceMethodCall c) { any() }
14+
15+
query predicate collections(ActiveResource::Collection c) { any() }
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
class Person < ActiveResource::Base
2+
self.site = "https://api.example.com"
3+
end
4+
5+
alice = Person.new(name: "Alice")
6+
alice.save
7+
8+
alice = Person.find(1)
9+
alice.address = "123 Main Street"
10+
alice.save
11+
12+
alice.destroy
13+
14+
# Custom REST methods
15+
16+
Person.new(name: "Bob").post(:register)
17+
alice.put(:promote)
18+
Person.get(:positions)
19+
alice.delete(:fire)
20+
21+
# Collections
22+
23+
people = Person.all
24+
people = Person.find(:all)
25+
26+
alice = people.first
27+
alice.save
28+
29+
class Post < ActiveResource::Base
30+
self.site = "http://api.insecure.com"
31+
end

0 commit comments

Comments
 (0)