Skip to content

Commit 30c9bea

Browse files
authored
Merge pull request #9974 from hmac/hmac/active-resource
Ruby: Model ActiveResource
2 parents 417b215 + 637e92d commit 30c9bea

File tree

7 files changed

+413
-0
lines changed

7 files changed

+413
-0
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: minorAnalysis
3+
---
4+
* Accesses of ActiveResource models are now recognized as HTTP requests.

ruby/ql/lib/codeql/ruby/Frameworks.qll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ private import codeql.ruby.frameworks.Core
66
private import codeql.ruby.frameworks.ActionCable
77
private import codeql.ruby.frameworks.ActionController
88
private import codeql.ruby.frameworks.ActiveRecord
9+
private import codeql.ruby.frameworks.ActiveResource
910
private import codeql.ruby.frameworks.ActiveStorage
1011
private import codeql.ruby.frameworks.ActionView
1112
private import codeql.ruby.frameworks.ActiveSupport

ruby/ql/lib/codeql/ruby/controlflow/CfgNodes.qll

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -711,6 +711,19 @@ module ExprNodes {
711711
final CfgNode getReceiver() { e.hasCfgChild(e.getReceiver(), this, result) }
712712
}
713713

714+
private class SelfVariableAccessMapping extends ExprChildMapping, SelfVariableAccess {
715+
override predicate relevantChild(AstNode n) { none() }
716+
}
717+
718+
/** A control-flow node that wraps a `SelfVariableAccess` AST expression. */
719+
class SelfVariableAccessCfgNode extends ExprCfgNode {
720+
final override string getAPrimaryQlClass() { result = "SelfVariableAccessCfgNode" }
721+
722+
override SelfVariableAccessMapping e;
723+
724+
override SelfVariableAccess getExpr() { result = ExprCfgNode.super.getExpr() }
725+
}
726+
714727
/** A control-flow node that wraps a `VariableWriteAccess` AST expression. */
715728
class VariableWriteAccessCfgNode extends ExprCfgNode {
716729
override string getAPrimaryQlClass() { result = "VariableWriteAccessCfgNode" }
Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
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+
/** Gets the API node for this model */
42+
API::Node getModelApiNode() { result = model }
43+
44+
/** Gets a call to `site=`, which sets the base URL for this model. */
45+
SiteAssignCall getASiteAssignment() { result.getModelClass() = this }
46+
47+
/** Holds if `c` sets a base URL which does not use HTTPS. */
48+
predicate disablesCertificateValidation(SiteAssignCall c) {
49+
c = this.getASiteAssignment() and
50+
c.disablesCertificateValidation()
51+
}
52+
}
53+
54+
/**
55+
* A call to a class method on an ActiveResource model class.
56+
*
57+
* ```rb
58+
* class Person < ActiveResource::Base
59+
* end
60+
*
61+
* Person.find(1)
62+
* ```
63+
*/
64+
class ModelClassMethodCall extends DataFlow::CallNode {
65+
API::Node model;
66+
67+
ModelClassMethodCall() {
68+
model = modelApiNode() and
69+
this = classMethodCall(model, _)
70+
}
71+
72+
/** Gets the model class for this call. */
73+
ModelClass getModelClass() { result.getModelApiNode() = model }
74+
}
75+
76+
/**
77+
* A call to `site=` on an ActiveResource model class.
78+
* This sets the base URL for all HTTP requests made by this class.
79+
*/
80+
private class SiteAssignCall extends DataFlow::CallNode {
81+
API::Node model;
82+
83+
SiteAssignCall() { model = modelApiNode() and this = classMethodCall(model, "site=") }
84+
85+
/**
86+
* Gets a node that contributes to the URLs used for HTTP requests by the parent
87+
* class.
88+
*/
89+
DataFlow::Node getAUrlPart() { result = this.getArgument(0) }
90+
91+
/** Gets the model class for this call. */
92+
ModelClass getModelClass() { result.getModelApiNode() = model }
93+
94+
/** Holds if this site value specifies HTTP rather than HTTPS. */
95+
predicate disablesCertificateValidation() {
96+
this.getAUrlPart()
97+
.asExpr()
98+
.(ExprNodes::AssignExprCfgNode)
99+
.getRhs()
100+
.getConstantValue()
101+
.getString()
102+
.regexpMatch("^http[^s].+")
103+
}
104+
}
105+
106+
/**
107+
* A call to the `find` class method, which returns an ActiveResource model
108+
* object.
109+
*
110+
* ```rb
111+
* alice = Person.find(1)
112+
* ```
113+
*/
114+
class FindCall extends ModelClassMethodCall {
115+
FindCall() { this.getMethodName() = "find" }
116+
}
117+
118+
/**
119+
* A call to the `create(!)` class method, which returns an ActiveResource model
120+
* object.
121+
*
122+
* ```rb
123+
* alice = Person.create(name: "alice")
124+
* ```
125+
*/
126+
class CreateCall extends ModelClassMethodCall {
127+
CreateCall() { this.getMethodName() = ["create", "create!"] }
128+
}
129+
130+
/**
131+
* A call to a method that sends a custom HTTP request outside of the
132+
* ActiveResource conventions. This typically returns an ActiveResource model
133+
* object. It may return a collection, but we don't currently model that.
134+
*
135+
* ```rb
136+
* alice = Person.get(:active)
137+
* ```
138+
*/
139+
class CustomHttpCall extends ModelClassMethodCall {
140+
CustomHttpCall() { this.getMethodName() = ["get", "post", "put", "patch", "delete"] }
141+
}
142+
143+
/**
144+
* An ActiveResource model object.
145+
*/
146+
class ModelInstance extends DataFlow::Node {
147+
ModelClass cls;
148+
149+
ModelInstance() {
150+
exists(API::Node model | model = modelApiNode() |
151+
this = model.getInstance().getAValueReachableFromSource() and
152+
cls.getModelApiNode() = model
153+
)
154+
or
155+
exists(FindCall call | call.flowsTo(this) | cls = call.getModelClass())
156+
or
157+
exists(CreateCall call | call.flowsTo(this) | cls = call.getModelClass())
158+
or
159+
exists(CustomHttpCall call | call.flowsTo(this) | cls = call.getModelClass())
160+
or
161+
exists(CollectionCall call |
162+
call.getMethodName() = ["first", "last"] and
163+
call.flowsTo(this)
164+
|
165+
cls = call.getCollection().getModelClass()
166+
)
167+
}
168+
169+
/** Gets the model class for this instance. */
170+
ModelClass getModelClass() { result = cls }
171+
}
172+
173+
/**
174+
* A call to a method on an ActiveResource model object.
175+
*/
176+
class ModelInstanceMethodCall extends DataFlow::CallNode {
177+
ModelInstance i;
178+
179+
ModelInstanceMethodCall() { this.getReceiver() = i }
180+
181+
/** Gets the model instance for this call. */
182+
ModelInstance getInstance() { result = i }
183+
184+
/** Gets the model class for this call. */
185+
ModelClass getModelClass() { result = i.getModelClass() }
186+
}
187+
188+
/**
189+
* A collection of ActiveResource model objects.
190+
*/
191+
class Collection extends DataFlow::Node {
192+
ModelClassMethodCall classMethodCall;
193+
194+
Collection() {
195+
classMethodCall.flowsTo(this) and
196+
(
197+
classMethodCall.getMethodName() = "all"
198+
or
199+
classMethodCall.getMethodName() = "find" and
200+
classMethodCall.getArgument(0).asExpr().getConstantValue().isStringlikeValue("all")
201+
)
202+
}
203+
204+
/** Gets the model class for this collection. */
205+
ModelClass getModelClass() { result = classMethodCall.getModelClass() }
206+
}
207+
208+
/**
209+
* A method call on a collection.
210+
*/
211+
class CollectionCall extends DataFlow::CallNode {
212+
CollectionCall() { this.getReceiver() instanceof Collection }
213+
214+
/** Gets the collection for this call. */
215+
Collection getCollection() { result = this.getReceiver() }
216+
}
217+
218+
private class ModelClassMethodCallAsHttpRequest extends HTTP::Client::Request::Range {
219+
ModelClassMethodCall call;
220+
ModelClass cls;
221+
222+
ModelClassMethodCallAsHttpRequest() {
223+
this = call.asExpr().getExpr() and
224+
call.getModelClass() = cls and
225+
call.getMethodName() = ["all", "build", "create", "create!", "find", "first", "last"]
226+
}
227+
228+
override string getFramework() { result = "ActiveResource" }
229+
230+
override predicate disablesCertificateValidation(DataFlow::Node disablingNode) {
231+
cls.disablesCertificateValidation(disablingNode)
232+
}
233+
234+
override DataFlow::Node getAUrlPart() { result = cls.getASiteAssignment().getAUrlPart() }
235+
236+
override DataFlow::Node getResponseBody() { result = call }
237+
}
238+
239+
private class ModelInstanceMethodCallAsHttpRequest extends HTTP::Client::Request::Range {
240+
ModelInstanceMethodCall call;
241+
ModelClass cls;
242+
243+
ModelInstanceMethodCallAsHttpRequest() {
244+
this = call.asExpr().getExpr() and
245+
call.getModelClass() = cls and
246+
call.getMethodName() =
247+
[
248+
"exists?", "reload", "save", "save!", "destroy", "delete", "get", "patch", "post", "put",
249+
"update_attribute", "update_attributes"
250+
]
251+
}
252+
253+
override string getFramework() { result = "ActiveResource" }
254+
255+
override predicate disablesCertificateValidation(DataFlow::Node disablingNode) {
256+
cls.disablesCertificateValidation(disablingNode)
257+
}
258+
259+
override DataFlow::Node getAUrlPart() { result = cls.getASiteAssignment().getAUrlPart() }
260+
261+
override DataFlow::Node getResponseBody() { result = call }
262+
}
263+
264+
/**
265+
* A call to a class method.
266+
*
267+
* TODO: is this general enough to be useful elsewhere?
268+
*
269+
* Examples:
270+
* ```rb
271+
* class A
272+
* def self.m; end
273+
*
274+
* m # call
275+
* end
276+
*
277+
* A.m # call
278+
* ```
279+
*/
280+
private DataFlow::CallNode classMethodCall(API::Node classNode, string methodName) {
281+
// A.m
282+
result = classNode.getAMethodCall(methodName)
283+
or
284+
// class A
285+
// A.m
286+
// end
287+
result.getReceiver().asExpr() instanceof ExprNodes::SelfVariableAccessCfgNode and
288+
result.asExpr().getExpr().getEnclosingModule().(ClassDeclaration).getSuperclassExpr() =
289+
classNode.getAValueReachableFromSource().asExpr().getExpr() and
290+
result.getMethodName() = methodName
291+
}
292+
}
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= | false |
3+
| active_resource.rb:29:1:31:3 | Post | active_resource.rb:30:3:30:11 | call to site= | true |
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: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import ruby
2+
import codeql.ruby.DataFlow
3+
import codeql.ruby.frameworks.ActiveResource
4+
5+
query predicate modelClasses(
6+
ActiveResource::ModelClass c, DataFlow::Node siteAssignCall, boolean disablesCertificateValidation
7+
) {
8+
c.getASiteAssignment() = siteAssignCall and
9+
if c.disablesCertificateValidation(siteAssignCall)
10+
then disablesCertificateValidation = true
11+
else disablesCertificateValidation = false
12+
}
13+
14+
query predicate modelClassMethodCalls(ActiveResource::ModelClassMethodCall c) { any() }
15+
16+
query predicate modelInstances(ActiveResource::ModelInstance c) { any() }
17+
18+
query predicate modelInstanceMethodCalls(ActiveResource::ModelInstanceMethodCall c) { any() }
19+
20+
query predicate collections(ActiveResource::Collection c) { any() }

0 commit comments

Comments
 (0)