Skip to content
Merged
Show file tree
Hide file tree
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 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
37e0c30
Add expected output for VariablesConsistency test case
felickz Sep 23, 2025
46d330c
Merge branch 'ruby-framework-grape' of github.com:felickz/codeql into…
felickz Sep 23, 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` have 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
332 changes: 332 additions & 0 deletions ruby/ql/lib/codeql/ruby/frameworks/Grape.qll
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 {
/**
* 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())
}
}
}

/**
* 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
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()
)
}
}

/**
* 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"
)
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"
)
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"
)
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())
)
}
}

/**
* 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())
)
}
}

/**
* 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"
)
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()
)
}

/**
* 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
)
}
}
Loading
Loading