Skip to content
Open
Show file tree
Hide file tree
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 Sep 12, 2025
738ab6f
Refactor Grape framework code for improved readability and consistency
felickz Sep 12, 2025
3252bd3
Enhance Grape framework with additional data flow modeling and helper…
felickz Sep 13, 2025
5cfa6e8
Add support for route parameters(+ blocks), headers, and cookies in G…
felickz Sep 13, 2025
a8d4d6b
Apply naming standards + changenote
felickz Sep 16, 2025
6cea939
Merge branch 'main' into ruby-framework-grape
felickz Sep 16, 2025
19cb187
Update ruby/ql/lib/codeql/ruby/frameworks/Grape.qll
felickz Sep 16, 2025
fc98cd8
Fix naming standards
felickz Sep 16, 2025
0d0ce32
Merge branch 'ruby-framework-grape' of github.com:felickz/codeql into…
felickz Sep 16, 2025
ffd32ef
codeql query format
felickz Sep 16, 2025
c5e3be2
Grape - detect params calls inside helper methods
felickz Sep 16, 2025
141b470
Merge branch 'main' into ruby-framework-grape
felickz Sep 17, 2025
89e9ee4
Convert from GrapeHelperMethodTaintStep extends AdditionalTaintStep …
felickz Sep 19, 2025
f4bbbc3
Refactor Grape framework to be encapsulated properly in Module
felickz Sep 19, 2025
50bf9ae
Refactor RootApi class to use getAnImmediateDescendent for clarity
felickz Sep 22, 2025
1bf6101
Remove redundant exclusion of base Grape::API module from GrapeApiClass
felickz Sep 22, 2025
b837c56
Refactor RootApi and GrapeApiClass constructors for improved readabil…
felickz Sep 22, 2025
ecd0ce6
Refactor GrapeHeadersBlockCall and GrapeCookiesBlockCall to simplify …
felickz Sep 22, 2025
0665c39
Refactor GrapeHelperMethod constructor to reuse getHelperSelf to trav…
felickz Sep 22, 2025
6e56c54
Refactor Grape method call classes to simplify handling of API instan…
felickz Sep 22, 2025
89fd969
codeql query format
felickz Sep 22, 2025
7a9a259
Merge branch 'main' into ruby-framework-grape
felickz Sep 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
1 change: 1 addition & 0 deletions ruby/ql/lib/codeql/ruby/Frameworks.qll
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ private import codeql.ruby.frameworks.Rails
private import codeql.ruby.frameworks.Railties
private import codeql.ruby.frameworks.Stdlib
private import codeql.ruby.frameworks.Files
private import codeql.ruby.frameworks.Grape
private import codeql.ruby.frameworks.HttpClients
private import codeql.ruby.frameworks.XmlParsing
private import codeql.ruby.frameworks.ActionDispatch
Expand Down
300 changes: 300 additions & 0 deletions ruby/ql/lib/codeql/ruby/frameworks/Grape.qll
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()
)
}
}
}
Loading
Loading