-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Ruby: Add support for Grape Framework #20427
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+856
−0
Merged
Changes from 12 commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
d295acc
Add initial support for Ruby Grape
felickz 738ab6f
Refactor Grape framework code for improved readability and consistency
felickz 3252bd3
Enhance Grape framework with additional data flow modeling and helper…
felickz 5cfa6e8
Add support for route parameters(+ blocks), headers, and cookies in G…
felickz a8d4d6b
Apply naming standards + changenote
felickz 6cea939
Merge branch 'main' into ruby-framework-grape
felickz 19cb187
Update ruby/ql/lib/codeql/ruby/frameworks/Grape.qll
felickz fc98cd8
Fix naming standards
felickz 0d0ce32
Merge branch 'ruby-framework-grape' of github.com:felickz/codeql into…
felickz ffd32ef
codeql query format
felickz c5e3be2
Grape - detect params calls inside helper methods
felickz 141b470
Merge branch 'main' into ruby-framework-grape
felickz 89e9ee4
Convert from GrapeHelperMethodTaintStep extends AdditionalTaintStep …
felickz f4bbbc3
Refactor Grape framework to be encapsulated properly in Module
felickz 50bf9ae
Refactor RootApi class to use getAnImmediateDescendent for clarity
felickz 1bf6101
Remove redundant exclusion of base Grape::API module from GrapeApiClass
felickz b837c56
Refactor RootApi and GrapeApiClass constructors for improved readabil…
felickz ecd0ce6
Refactor GrapeHeadersBlockCall and GrapeCookiesBlockCall to simplify …
felickz 0665c39
Refactor GrapeHelperMethod constructor to reuse getHelperSelf to trav…
felickz 6e56c54
Refactor Grape method call classes to simplify handling of API instan…
felickz 89fd969
codeql query format
felickz 7a9a259
Merge branch 'main' into ruby-framework-grape
felickz 37e0c30
Add expected output for VariablesConsistency test case
felickz 46d330c
Merge branch 'ruby-framework-grape' of github.com:felickz/codeql into…
felickz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
4 changes: 4 additions & 0 deletions
4
ruby/ql/lib/change-notes/2025-09-15-grape-framework-support.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
--- | ||
category: feature | ||
--- | ||
* Initial modeling for the Ruby Grape framework in `Grape.qll` have been added to detect API endpoints, parameters, and headers within Grape API classes. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,332 @@ | ||
/** | ||
* Provides modeling for the `Grape` API framework. | ||
*/ | ||
|
||
private import codeql.ruby.AST | ||
private import codeql.ruby.Concepts | ||
private import codeql.ruby.controlflow.CfgNodes | ||
private import codeql.ruby.DataFlow | ||
private import codeql.ruby.dataflow.RemoteFlowSources | ||
private import codeql.ruby.ApiGraphs | ||
private import codeql.ruby.typetracking.TypeTracking | ||
private import codeql.ruby.frameworks.Rails | ||
private import codeql.ruby.frameworks.internal.Rails | ||
private import codeql.ruby.dataflow.internal.DataFlowDispatch | ||
private import codeql.ruby.dataflow.FlowSteps | ||
|
||
/** | ||
* Provides modeling for Grape, a REST-like API framework for Ruby. | ||
* Grape allows you to build RESTful APIs in Ruby with minimal effort. | ||
*/ | ||
module Grape { | ||
felickz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/** | ||
* A Grape API class which sits at the top of the class hierarchy. | ||
* In other words, it does not subclass any other Grape API class in source code. | ||
*/ | ||
class RootApi extends GrapeApiClass { | ||
RootApi() { | ||
not exists(GrapeApiClass parent | this != parent and this = parent.getADescendent()) | ||
felickz marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
} | ||
} | ||
} | ||
|
||
/** | ||
* A class that extends `Grape::API`. | ||
* For example, | ||
* | ||
* ```rb | ||
* class FooAPI < Grape::API | ||
* get '/users' do | ||
* name = params[:name] | ||
* User.where("name = #{name}") | ||
* end | ||
* end | ||
* ``` | ||
*/ | ||
class GrapeApiClass extends DataFlow::ClassNode { | ||
GrapeApiClass() { | ||
this = grapeApiBaseClass().getADescendentModule() and | ||
felickz marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
not exists(DataFlow::ModuleNode m | m = grapeApiBaseClass().asModule() | this = m) | ||
} | ||
|
||
/** | ||
* Gets a `GrapeEndpoint` defined in this class. | ||
*/ | ||
GrapeEndpoint getAnEndpoint() { result.getApiClass() = this } | ||
|
||
/** | ||
* Gets a `self` that possibly refers to an instance of this class. | ||
*/ | ||
DataFlow::LocalSourceNode getSelf() { | ||
result = this.getAnInstanceSelf() | ||
or | ||
// Include the module-level `self` to recover some cases where a block at the module level | ||
// is invoked with an instance as the `self`. | ||
result = this.getModuleLevelSelf() | ||
} | ||
} | ||
|
||
private DataFlow::ConstRef grapeApiBaseClass() { | ||
result = DataFlow::getConstant("Grape").getConstant("API") | ||
} | ||
|
||
private API::Node grapeApiInstance() { result = any(GrapeApiClass cls).getSelf().track() } | ||
|
||
/** | ||
* A Grape API endpoint (get, post, put, delete, etc.) call within a `Grape::API` class. | ||
*/ | ||
class GrapeEndpoint extends DataFlow::CallNode { | ||
private GrapeApiClass apiClass; | ||
|
||
GrapeEndpoint() { | ||
this = | ||
apiClass.getAModuleLevelCall(["get", "post", "put", "delete", "patch", "head", "options"]) | ||
} | ||
|
||
/** | ||
* Gets the HTTP method for this endpoint (e.g., "GET", "POST", etc.) | ||
*/ | ||
string getHttpMethod() { result = this.getMethodName().toUpperCase() } | ||
|
||
/** | ||
* Gets the API class containing this endpoint. | ||
*/ | ||
GrapeApiClass getApiClass() { result = apiClass } | ||
|
||
/** | ||
* Gets the block containing the endpoint logic. | ||
*/ | ||
DataFlow::BlockNode getBody() { result = this.getBlock() } | ||
|
||
/** | ||
* Gets the path pattern for this endpoint, if specified. | ||
*/ | ||
string getPath() { result = this.getArgument(0).getConstantValue().getString() } | ||
} | ||
|
||
/** | ||
* A `RemoteFlowSource::Range` to represent accessing the | ||
* Grape parameters available via the `params` method within an endpoint. | ||
*/ | ||
class GrapeParamsSource extends Http::Server::RequestInputAccess::Range { | ||
GrapeParamsSource() { this.asExpr().getExpr() instanceof GrapeParamsCall } | ||
|
||
override string getSourceType() { result = "Grape::API#params" } | ||
|
||
override Http::Server::RequestInputKind getKind() { result = Http::Server::parameterInputKind() } | ||
} | ||
|
||
/** | ||
* A call to `params` from within a Grape API endpoint or helper method. | ||
*/ | ||
private class GrapeParamsCall extends ParamsCallImpl { | ||
GrapeParamsCall() { | ||
// Params calls within endpoint blocks | ||
exists(GrapeApiClass api | | ||
this.getMethodName() = "params" and | ||
this.getParent+() = api.getADeclaration() | ||
) | ||
or | ||
// Params calls within helper methods (defined in helpers blocks) | ||
exists(GrapeApiClass api, DataFlow::CallNode helpersCall | | ||
helpersCall = api.getAModuleLevelCall("helpers") and | ||
this.getMethodName() = "params" and | ||
this.getParent+() = helpersCall.getBlock().asExpr().getExpr() | ||
) | ||
felickz marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
} | ||
} | ||
|
||
/** | ||
* A call to `headers` from within a Grape API endpoint or headers block. | ||
* Headers can also be a source of user input. | ||
*/ | ||
class GrapeHeadersSource extends Http::Server::RequestInputAccess::Range { | ||
GrapeHeadersSource() { | ||
this.asExpr().getExpr() instanceof GrapeHeadersCall | ||
or | ||
this.asExpr().getExpr() instanceof GrapeHeadersBlockCall | ||
} | ||
|
||
override string getSourceType() { result = "Grape::API#headers" } | ||
|
||
override Http::Server::RequestInputKind getKind() { result = Http::Server::headerInputKind() } | ||
} | ||
|
||
/** | ||
* A call to `headers` from within a Grape API endpoint. | ||
*/ | ||
private class GrapeHeadersCall extends MethodCall { | ||
GrapeHeadersCall() { | ||
exists(GrapeEndpoint endpoint | | ||
this.getParent+() = endpoint.getBody().asCallableAstNode() and | ||
this.getMethodName() = "headers" | ||
) | ||
felickz marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
or | ||
// Also handle cases where headers is called on an instance of a Grape API class | ||
this = grapeApiInstance().getAMethodCall("headers").asExpr().getExpr() | ||
} | ||
} | ||
|
||
/** | ||
* A call to `request` from within a Grape API endpoint. | ||
* The request object can contain user input. | ||
*/ | ||
class GrapeRequestSource extends Http::Server::RequestInputAccess::Range { | ||
GrapeRequestSource() { this.asExpr().getExpr() instanceof GrapeRequestCall } | ||
|
||
override string getSourceType() { result = "Grape::API#request" } | ||
|
||
override Http::Server::RequestInputKind getKind() { result = Http::Server::parameterInputKind() } | ||
} | ||
|
||
/** | ||
* A call to `route_param` from within a Grape API endpoint. | ||
* Route parameters are extracted from the URL path and can be a source of user input. | ||
*/ | ||
class GrapeRouteParamSource extends Http::Server::RequestInputAccess::Range { | ||
GrapeRouteParamSource() { this.asExpr().getExpr() instanceof GrapeRouteParamCall } | ||
|
||
override string getSourceType() { result = "Grape::API#route_param" } | ||
|
||
override Http::Server::RequestInputKind getKind() { result = Http::Server::parameterInputKind() } | ||
} | ||
|
||
/** | ||
* A call to `request` from within a Grape API endpoint. | ||
*/ | ||
private class GrapeRequestCall extends MethodCall { | ||
GrapeRequestCall() { | ||
exists(GrapeEndpoint endpoint | | ||
this.getParent+() = endpoint.getBody().asCallableAstNode() and | ||
this.getMethodName() = "request" | ||
) | ||
felickz marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
or | ||
// Also handle cases where request is called on an instance of a Grape API class | ||
this = grapeApiInstance().getAMethodCall("request").asExpr().getExpr() | ||
} | ||
} | ||
|
||
/** | ||
* A call to `route_param` from within a Grape API endpoint. | ||
*/ | ||
private class GrapeRouteParamCall extends MethodCall { | ||
GrapeRouteParamCall() { | ||
exists(GrapeEndpoint endpoint | | ||
this.getParent+() = endpoint.getBody().asExpr().getExpr() and | ||
this.getMethodName() = "route_param" | ||
) | ||
felickz marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
or | ||
// Also handle cases where route_param is called on an instance of a Grape API class | ||
this = grapeApiInstance().getAMethodCall("route_param").asExpr().getExpr() | ||
} | ||
} | ||
|
||
/** | ||
* A call to `headers` block within a Grape API class. | ||
* This is different from the headers() method call - this is the DSL block for defining header requirements. | ||
*/ | ||
private class GrapeHeadersBlockCall extends MethodCall { | ||
GrapeHeadersBlockCall() { | ||
exists(GrapeApiClass api | | ||
this.getParent+() = api.getADeclaration() and | ||
this.getMethodName() = "headers" and | ||
exists(this.getBlock()) | ||
) | ||
felickz marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
} | ||
} | ||
|
||
/** | ||
* A call to `cookies` block within a Grape API class. | ||
* This DSL block defines cookie requirements and those cookies are user-controlled. | ||
*/ | ||
private class GrapeCookiesBlockCall extends MethodCall { | ||
GrapeCookiesBlockCall() { | ||
exists(GrapeApiClass api | | ||
this.getParent+() = api.getADeclaration() and | ||
this.getMethodName() = "cookies" and | ||
exists(this.getBlock()) | ||
) | ||
felickz marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
} | ||
} | ||
|
||
/** | ||
* A call to `cookies` method from within a Grape API endpoint or cookies block. | ||
* Similar to headers, cookies can be accessed as a method and are user-controlled input. | ||
*/ | ||
class GrapeCookiesSource extends Http::Server::RequestInputAccess::Range { | ||
GrapeCookiesSource() { | ||
this.asExpr().getExpr() instanceof GrapeCookiesCall | ||
or | ||
this.asExpr().getExpr() instanceof GrapeCookiesBlockCall | ||
} | ||
|
||
override string getSourceType() { result = "Grape::API#cookies" } | ||
|
||
override Http::Server::RequestInputKind getKind() { result = Http::Server::cookieInputKind() } | ||
} | ||
|
||
/** | ||
* A call to `cookies` method from within a Grape API endpoint. | ||
*/ | ||
private class GrapeCookiesCall extends MethodCall { | ||
GrapeCookiesCall() { | ||
exists(GrapeEndpoint endpoint | | ||
this.getParent+() = endpoint.getBody().asCallableAstNode() and | ||
this.getMethodName() = "cookies" | ||
) | ||
felickz marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
or | ||
// Also handle cases where cookies is called on an instance of a Grape API class | ||
this = grapeApiInstance().getAMethodCall("cookies").asExpr().getExpr() | ||
} | ||
} | ||
|
||
/** | ||
* A method defined within a `helpers` block in a Grape API class. | ||
* These methods become available in endpoint contexts through Grape's DSL. | ||
*/ | ||
private class GrapeHelperMethod extends Method { | ||
private GrapeApiClass apiClass; | ||
|
||
GrapeHelperMethod() { | ||
exists(DataFlow::CallNode helpersCall | | ||
helpersCall = apiClass.getAModuleLevelCall("helpers") and | ||
this.getParent+() = helpersCall.getBlock().asExpr().getExpr() | ||
) | ||
} | ||
felickz marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
||
/** | ||
* Gets the API class that contains this helper method. | ||
|
||
*/ | ||
GrapeApiClass getApiClass() { result = apiClass } | ||
} | ||
|
||
/** | ||
* Additional taint step to model dataflow from method arguments to parameters | ||
* and from return values back to call sites for Grape helper methods defined in `helpers` blocks. | ||
* This bridges the gap where standard dataflow doesn't recognize the Grape DSL semantics. | ||
*/ | ||
private class GrapeHelperMethodTaintStep extends AdditionalTaintStep { | ||
override predicate step(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) { | ||
// Map arguments to parameters for helper method calls | ||
exists(GrapeHelperMethod helperMethod, MethodCall call, int i | | ||
// Find calls to helper methods from within Grape endpoints or other helper methods | ||
call.getMethodName() = helperMethod.getName() and | ||
exists(GrapeApiClass api | call.getParent+() = api.getADeclaration()) and | ||
// Map argument to parameter | ||
nodeFrom.asExpr().getExpr() = call.getArgument(i) and | ||
nodeTo.asParameter() = helperMethod.getParameter(i) | ||
) | ||
or | ||
// Model implicit return values: the last expression in a helper method flows to the call site | ||
exists(GrapeHelperMethod helperMethod, MethodCall helperCall, Expr lastExpr | | ||
// Find calls to helper methods from within Grape endpoints or other helper methods | ||
helperCall.getMethodName() = helperMethod.getName() and | ||
exists(GrapeApiClass api | helperCall.getParent+() = api.getADeclaration()) and | ||
// Get the last expression in the helper method (Ruby's implicit return) | ||
lastExpr = helperMethod.getLastStmt() and | ||
// Flow from the last expression in the helper method to the call site | ||
nodeFrom.asExpr().getExpr() = lastExpr and | ||
nodeTo.asExpr().getExpr() = helperCall | ||
) | ||
} | ||
} | ||
felickz marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.