-
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
Open
felickz
wants to merge
22
commits into
github:main
Choose a base branch
from
felickz:ruby-framework-grape
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+852
−0
Open
Changes from all commits
Commits
Show all changes
22 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 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
Some comments aren't visible on the classic Files Changed page.
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` has 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,300 @@ | ||
/** | ||
* Provides modeling for the `Grape` API framework. | ||
*/ | ||
|
||
private import codeql.ruby.AST | ||
private import codeql.ruby.CFG | ||
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 { | ||
/** | ||
* 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 this = any(GrapeApiClass parent).getAnImmediateDescendent() } | ||
} | ||
|
||
/** | ||
* 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() } | ||
|
||
/** | ||
* 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() | ||
} | ||
|
||
/** | ||
* Gets the `self` parameter belonging to a method defined within a | ||
* `helpers` block in this API class. | ||
* | ||
* These methods become available in endpoint contexts through Grape's DSL. | ||
*/ | ||
DataFlow::SelfParameterNode getHelperSelf() { | ||
exists(DataFlow::CallNode helpersCall | | ||
helpersCall = this.getAModuleLevelCall("helpers") and | ||
result.getSelfVariable().getDeclaringScope().getOuterScope+() = | ||
helpersCall.getBlock().asExpr().getExpr() | ||
) | ||
} | ||
} | ||
|
||
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() { | ||
exists(API::Node n | this = n.getAMethodCall("params").asExpr().getExpr() | | ||
// Params calls within endpoint blocks | ||
n = grapeApiInstance() | ||
or | ||
// Params calls within helper methods (defined in helpers blocks) | ||
n = any(GrapeApiClass c).getHelperSelf().track() | ||
) | ||
} | ||
} | ||
|
||
/** | ||
* 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() { | ||
// 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() { | ||
// 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() { | ||
// 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() { | ||
this = grapeApiInstance().getAMethodCall("headers").asExpr().getExpr() and | ||
exists(this.getBlock()) | ||
} | ||
} | ||
|
||
/** | ||
* 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() { | ||
this = grapeApiInstance().getAMethodCall("cookies").asExpr().getExpr() and | ||
exists(this.getBlock()) | ||
} | ||
} | ||
|
||
/** | ||
* 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() { | ||
// 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() { this = apiClass.getHelperSelf().getSelfVariable().getDeclaringScope() } | ||
|
||
/** | ||
* Gets the API class that contains this helper method. | ||
*/ | ||
GrapeApiClass getApiClass() { result = apiClass } | ||
} | ||
|
||
/** | ||
* Additional call-target to resolve helper method calls defined in `helpers` blocks. | ||
* | ||
* This class is responsible for resolving calls to helper methods defined in | ||
* `helpers` blocks, allowing the dataflow framework to accurately track | ||
* the flow of information between these methods and their call sites. | ||
*/ | ||
private class GrapeHelperMethodTarget extends AdditionalCallTarget { | ||
override DataFlowCallable viableTarget(CfgNodes::ExprNodes::CallCfgNode call) { | ||
// Find calls to helper methods from within Grape endpoints or other helper methods | ||
exists(GrapeHelperMethod helperMethod, MethodCall mc | | ||
result.asCfgScope() = helperMethod and | ||
mc = call.getAstNode() and | ||
mc.getMethodName() = helperMethod.getName() and | ||
mc.getParent+() = helperMethod.getApiClass().getADeclaration() | ||
) | ||
} | ||
} | ||
} |
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.