Refactor Grape framework to be encapsulated properly in Module

This commit is contained in:
Chad Bentz
2025-09-19 19:06:50 -04:00
parent 89e9ee43c0
commit f4bbbc346f
2 changed files with 285 additions and 279 deletions

View File

@@ -29,293 +29,299 @@ module Grape {
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)
/**
* 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() }
}
/**
* Gets a `GrapeEndpoint` defined in this class.
* A `RemoteFlowSource::Range` to represent accessing the
* Grape parameters available via the `params` method within an endpoint.
*/
GrapeEndpoint getAnEndpoint() { result.getApiClass() = this }
class GrapeParamsSource extends Http::Server::RequestInputAccess::Range {
GrapeParamsSource() { this.asExpr().getExpr() instanceof GrapeParamsCall }
/**
* 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()
}
}
override string getSourceType() { result = "Grape::API#params" }
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"])
override Http::Server::RequestInputKind getKind() {
result = Http::Server::parameterInputKind()
}
}
/**
* Gets the HTTP method for this endpoint (e.g., "GET", "POST", etc.)
* A call to `params` from within a Grape API endpoint or helper method.
*/
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()
)
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()
)
}
}
/**
* Gets the API class that contains this helper method.
* A call to `headers` from within a Grape API endpoint or headers block.
* Headers can also be a source of user input.
*/
GrapeApiClass getApiClass() { result = apiClass }
}
class GrapeHeadersSource extends Http::Server::RequestInputAccess::Range {
GrapeHeadersSource() {
this.asExpr().getExpr() instanceof GrapeHeadersCall
or
this.asExpr().getExpr() instanceof GrapeHeadersBlockCall
}
/**
* 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()
)
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 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()
)
}
}
}

View File

@@ -3,20 +3,20 @@ import codeql.ruby.frameworks.Grape
import codeql.ruby.Concepts
import codeql.ruby.AST
query predicate grapeApiClasses(GrapeApiClass api) { any() }
query predicate grapeApiClasses(Grape::GrapeApiClass api) { any() }
query predicate grapeEndpoints(GrapeApiClass api, GrapeEndpoint endpoint, string method, string path) {
query predicate grapeEndpoints(Grape::GrapeApiClass api, Grape::GrapeEndpoint endpoint, string method, string path) {
endpoint = api.getAnEndpoint() and
method = endpoint.getHttpMethod() and
path = endpoint.getPath()
}
query predicate grapeParams(GrapeParamsSource params) { any() }
query predicate grapeParams(Grape::GrapeParamsSource params) { any() }
query predicate grapeHeaders(GrapeHeadersSource headers) { any() }
query predicate grapeHeaders(Grape::GrapeHeadersSource headers) { any() }
query predicate grapeRequest(GrapeRequestSource request) { any() }
query predicate grapeRequest(Grape::GrapeRequestSource request) { any() }
query predicate grapeRouteParam(GrapeRouteParamSource routeParam) { any() }
query predicate grapeRouteParam(Grape::GrapeRouteParamSource routeParam) { any() }
query predicate grapeCookies(GrapeCookiesSource cookies) { any() }
query predicate grapeCookies(Grape::GrapeCookiesSource cookies) { any() }