Skip to content

Commit 1a4cfba

Browse files
authored
Merge pull request #20427 from felickz/ruby-framework-grape
Ruby: Add support for Grape Framework
2 parents e592fd6 + 46d330c commit 1a4cfba

File tree

9 files changed

+856
-0
lines changed

9 files changed

+856
-0
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: feature
3+
---
4+
* Initial modeling for the Ruby Grape framework in `Grape.qll` has been added to detect API endpoints, parameters, and headers within Grape API classes.

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ private import codeql.ruby.frameworks.Rails
2121
private import codeql.ruby.frameworks.Railties
2222
private import codeql.ruby.frameworks.Stdlib
2323
private import codeql.ruby.frameworks.Files
24+
private import codeql.ruby.frameworks.Grape
2425
private import codeql.ruby.frameworks.HttpClients
2526
private import codeql.ruby.frameworks.XmlParsing
2627
private import codeql.ruby.frameworks.ActionDispatch
Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
/**
2+
* Provides modeling for the `Grape` API framework.
3+
*/
4+
5+
private import codeql.ruby.AST
6+
private import codeql.ruby.CFG
7+
private import codeql.ruby.Concepts
8+
private import codeql.ruby.controlflow.CfgNodes
9+
private import codeql.ruby.DataFlow
10+
private import codeql.ruby.dataflow.RemoteFlowSources
11+
private import codeql.ruby.ApiGraphs
12+
private import codeql.ruby.typetracking.TypeTracking
13+
private import codeql.ruby.frameworks.Rails
14+
private import codeql.ruby.frameworks.internal.Rails
15+
private import codeql.ruby.dataflow.internal.DataFlowDispatch
16+
private import codeql.ruby.dataflow.FlowSteps
17+
18+
/**
19+
* Provides modeling for Grape, a REST-like API framework for Ruby.
20+
* Grape allows you to build RESTful APIs in Ruby with minimal effort.
21+
*/
22+
module Grape {
23+
/**
24+
* A Grape API class which sits at the top of the class hierarchy.
25+
* In other words, it does not subclass any other Grape API class in source code.
26+
*/
27+
class RootApi extends GrapeApiClass {
28+
RootApi() { not this = any(GrapeApiClass parent).getAnImmediateDescendent() }
29+
}
30+
31+
/**
32+
* A class that extends `Grape::API`.
33+
* For example,
34+
*
35+
* ```rb
36+
* class FooAPI < Grape::API
37+
* get '/users' do
38+
* name = params[:name]
39+
* User.where("name = #{name}")
40+
* end
41+
* end
42+
* ```
43+
*/
44+
class GrapeApiClass extends DataFlow::ClassNode {
45+
GrapeApiClass() { this = grapeApiBaseClass().getADescendentModule() }
46+
47+
/**
48+
* Gets a `GrapeEndpoint` defined in this class.
49+
*/
50+
GrapeEndpoint getAnEndpoint() { result.getApiClass() = this }
51+
52+
/**
53+
* Gets a `self` that possibly refers to an instance of this class.
54+
*/
55+
DataFlow::LocalSourceNode getSelf() {
56+
result = this.getAnInstanceSelf()
57+
or
58+
// Include the module-level `self` to recover some cases where a block at the module level
59+
// is invoked with an instance as the `self`.
60+
result = this.getModuleLevelSelf()
61+
}
62+
63+
/**
64+
* Gets the `self` parameter belonging to a method defined within a
65+
* `helpers` block in this API class.
66+
*
67+
* These methods become available in endpoint contexts through Grape's DSL.
68+
*/
69+
DataFlow::SelfParameterNode getHelperSelf() {
70+
exists(DataFlow::CallNode helpersCall |
71+
helpersCall = this.getAModuleLevelCall("helpers") and
72+
result.getSelfVariable().getDeclaringScope().getOuterScope+() =
73+
helpersCall.getBlock().asExpr().getExpr()
74+
)
75+
}
76+
}
77+
78+
private DataFlow::ConstRef grapeApiBaseClass() {
79+
result = DataFlow::getConstant("Grape").getConstant("API")
80+
}
81+
82+
private API::Node grapeApiInstance() { result = any(GrapeApiClass cls).getSelf().track() }
83+
84+
/**
85+
* A Grape API endpoint (get, post, put, delete, etc.) call within a `Grape::API` class.
86+
*/
87+
class GrapeEndpoint extends DataFlow::CallNode {
88+
private GrapeApiClass apiClass;
89+
90+
GrapeEndpoint() {
91+
this =
92+
apiClass.getAModuleLevelCall(["get", "post", "put", "delete", "patch", "head", "options"])
93+
}
94+
95+
/**
96+
* Gets the HTTP method for this endpoint (e.g., "GET", "POST", etc.)
97+
*/
98+
string getHttpMethod() { result = this.getMethodName().toUpperCase() }
99+
100+
/**
101+
* Gets the API class containing this endpoint.
102+
*/
103+
GrapeApiClass getApiClass() { result = apiClass }
104+
105+
/**
106+
* Gets the block containing the endpoint logic.
107+
*/
108+
DataFlow::BlockNode getBody() { result = this.getBlock() }
109+
110+
/**
111+
* Gets the path pattern for this endpoint, if specified.
112+
*/
113+
string getPath() { result = this.getArgument(0).getConstantValue().getString() }
114+
}
115+
116+
/**
117+
* A `RemoteFlowSource::Range` to represent accessing the
118+
* Grape parameters available via the `params` method within an endpoint.
119+
*/
120+
class GrapeParamsSource extends Http::Server::RequestInputAccess::Range {
121+
GrapeParamsSource() { this.asExpr().getExpr() instanceof GrapeParamsCall }
122+
123+
override string getSourceType() { result = "Grape::API#params" }
124+
125+
override Http::Server::RequestInputKind getKind() {
126+
result = Http::Server::parameterInputKind()
127+
}
128+
}
129+
130+
/**
131+
* A call to `params` from within a Grape API endpoint or helper method.
132+
*/
133+
private class GrapeParamsCall extends ParamsCallImpl {
134+
GrapeParamsCall() {
135+
exists(API::Node n | this = n.getAMethodCall("params").asExpr().getExpr() |
136+
// Params calls within endpoint blocks
137+
n = grapeApiInstance()
138+
or
139+
// Params calls within helper methods (defined in helpers blocks)
140+
n = any(GrapeApiClass c).getHelperSelf().track()
141+
)
142+
}
143+
}
144+
145+
/**
146+
* A call to `headers` from within a Grape API endpoint or headers block.
147+
* Headers can also be a source of user input.
148+
*/
149+
class GrapeHeadersSource extends Http::Server::RequestInputAccess::Range {
150+
GrapeHeadersSource() {
151+
this.asExpr().getExpr() instanceof GrapeHeadersCall
152+
or
153+
this.asExpr().getExpr() instanceof GrapeHeadersBlockCall
154+
}
155+
156+
override string getSourceType() { result = "Grape::API#headers" }
157+
158+
override Http::Server::RequestInputKind getKind() { result = Http::Server::headerInputKind() }
159+
}
160+
161+
/**
162+
* A call to `headers` from within a Grape API endpoint.
163+
*/
164+
private class GrapeHeadersCall extends MethodCall {
165+
GrapeHeadersCall() {
166+
// Handle cases where headers is called on an instance of a Grape API class
167+
this = grapeApiInstance().getAMethodCall("headers").asExpr().getExpr()
168+
}
169+
}
170+
171+
/**
172+
* A call to `request` from within a Grape API endpoint.
173+
* The request object can contain user input.
174+
*/
175+
class GrapeRequestSource extends Http::Server::RequestInputAccess::Range {
176+
GrapeRequestSource() { this.asExpr().getExpr() instanceof GrapeRequestCall }
177+
178+
override string getSourceType() { result = "Grape::API#request" }
179+
180+
override Http::Server::RequestInputKind getKind() {
181+
result = Http::Server::parameterInputKind()
182+
}
183+
}
184+
185+
/**
186+
* A call to `route_param` from within a Grape API endpoint.
187+
* Route parameters are extracted from the URL path and can be a source of user input.
188+
*/
189+
class GrapeRouteParamSource extends Http::Server::RequestInputAccess::Range {
190+
GrapeRouteParamSource() { this.asExpr().getExpr() instanceof GrapeRouteParamCall }
191+
192+
override string getSourceType() { result = "Grape::API#route_param" }
193+
194+
override Http::Server::RequestInputKind getKind() {
195+
result = Http::Server::parameterInputKind()
196+
}
197+
}
198+
199+
/**
200+
* A call to `request` from within a Grape API endpoint.
201+
*/
202+
private class GrapeRequestCall extends MethodCall {
203+
GrapeRequestCall() {
204+
// Handle cases where request is called on an instance of a Grape API class
205+
this = grapeApiInstance().getAMethodCall("request").asExpr().getExpr()
206+
}
207+
}
208+
209+
/**
210+
* A call to `route_param` from within a Grape API endpoint.
211+
*/
212+
private class GrapeRouteParamCall extends MethodCall {
213+
GrapeRouteParamCall() {
214+
// Handle cases where route_param is called on an instance of a Grape API class
215+
this = grapeApiInstance().getAMethodCall("route_param").asExpr().getExpr()
216+
}
217+
}
218+
219+
/**
220+
* A call to `headers` block within a Grape API class.
221+
* This is different from the headers() method call - this is the DSL block for defining header requirements.
222+
*/
223+
private class GrapeHeadersBlockCall extends MethodCall {
224+
GrapeHeadersBlockCall() {
225+
this = grapeApiInstance().getAMethodCall("headers").asExpr().getExpr() and
226+
exists(this.getBlock())
227+
}
228+
}
229+
230+
/**
231+
* A call to `cookies` block within a Grape API class.
232+
* This DSL block defines cookie requirements and those cookies are user-controlled.
233+
*/
234+
private class GrapeCookiesBlockCall extends MethodCall {
235+
GrapeCookiesBlockCall() {
236+
this = grapeApiInstance().getAMethodCall("cookies").asExpr().getExpr() and
237+
exists(this.getBlock())
238+
}
239+
}
240+
241+
/**
242+
* A call to `cookies` method from within a Grape API endpoint or cookies block.
243+
* Similar to headers, cookies can be accessed as a method and are user-controlled input.
244+
*/
245+
class GrapeCookiesSource extends Http::Server::RequestInputAccess::Range {
246+
GrapeCookiesSource() {
247+
this.asExpr().getExpr() instanceof GrapeCookiesCall
248+
or
249+
this.asExpr().getExpr() instanceof GrapeCookiesBlockCall
250+
}
251+
252+
override string getSourceType() { result = "Grape::API#cookies" }
253+
254+
override Http::Server::RequestInputKind getKind() { result = Http::Server::cookieInputKind() }
255+
}
256+
257+
/**
258+
* A call to `cookies` method from within a Grape API endpoint.
259+
*/
260+
private class GrapeCookiesCall extends MethodCall {
261+
GrapeCookiesCall() {
262+
// Handle cases where cookies is called on an instance of a Grape API class
263+
this = grapeApiInstance().getAMethodCall("cookies").asExpr().getExpr()
264+
}
265+
}
266+
267+
/**
268+
* A method defined within a `helpers` block in a Grape API class.
269+
* These methods become available in endpoint contexts through Grape's DSL.
270+
*/
271+
private class GrapeHelperMethod extends Method {
272+
private GrapeApiClass apiClass;
273+
274+
GrapeHelperMethod() { this = apiClass.getHelperSelf().getSelfVariable().getDeclaringScope() }
275+
276+
/**
277+
* Gets the API class that contains this helper method.
278+
*/
279+
GrapeApiClass getApiClass() { result = apiClass }
280+
}
281+
282+
/**
283+
* Additional call-target to resolve helper method calls defined in `helpers` blocks.
284+
*
285+
* This class is responsible for resolving calls to helper methods defined in
286+
* `helpers` blocks, allowing the dataflow framework to accurately track
287+
* the flow of information between these methods and their call sites.
288+
*/
289+
private class GrapeHelperMethodTarget extends AdditionalCallTarget {
290+
override DataFlowCallable viableTarget(CfgNodes::ExprNodes::CallCfgNode call) {
291+
// Find calls to helper methods from within Grape endpoints or other helper methods
292+
exists(GrapeHelperMethod helperMethod, MethodCall mc |
293+
result.asCfgScope() = helperMethod and
294+
mc = call.getAstNode() and
295+
mc.getMethodName() = helperMethod.getName() and
296+
mc.getParent+() = helperMethod.getApiClass().getADeclaration()
297+
)
298+
}
299+
}
300+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
variableIsCaptured
2+
| app.rb:126:9:130:11 | self | CapturedVariable is not captured |
3+
consistencyOverview
4+
| CapturedVariable is not captured | 1 |

0 commit comments

Comments
 (0)