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,9 +29,8 @@ module Grape {
not exists(GrapeApiClass parent | this != parent and this = parent.getADescendent())
}
}
}
/**
/**
* A class that extends `Grape::API`.
* For example,
*
@@ -44,7 +43,7 @@ module Grape {
* end
* ```
*/
class GrapeApiClass extends DataFlow::ClassNode {
class GrapeApiClass extends DataFlow::ClassNode {
GrapeApiClass() {
this = grapeApiBaseClass().getADescendentModule() and
not exists(DataFlow::ModuleNode m | m = grapeApiBaseClass().asModule() | this = m)
@@ -65,18 +64,18 @@ class GrapeApiClass extends DataFlow::ClassNode {
// is invoked with an instance as the `self`.
result = this.getModuleLevelSelf()
}
}
}
private DataFlow::ConstRef grapeApiBaseClass() {
private DataFlow::ConstRef grapeApiBaseClass() {
result = DataFlow::getConstant("Grape").getConstant("API")
}
}
private API::Node grapeApiInstance() { result = any(GrapeApiClass cls).getSelf().track() }
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 {
class GrapeEndpoint extends DataFlow::CallNode {
private GrapeApiClass apiClass;
GrapeEndpoint() {
@@ -103,24 +102,26 @@ class GrapeEndpoint extends DataFlow::CallNode {
* 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 {
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() }
}
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 {
private class GrapeParamsCall extends ParamsCallImpl {
GrapeParamsCall() {
// Params calls within endpoint blocks
exists(GrapeApiClass api |
@@ -135,13 +136,13 @@ private class GrapeParamsCall extends ParamsCallImpl {
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 {
class GrapeHeadersSource extends Http::Server::RequestInputAccess::Range {
GrapeHeadersSource() {
this.asExpr().getExpr() instanceof GrapeHeadersCall
or
@@ -151,12 +152,12 @@ class GrapeHeadersSource extends Http::Server::RequestInputAccess::Range {
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 {
private class GrapeHeadersCall extends MethodCall {
GrapeHeadersCall() {
exists(GrapeEndpoint endpoint |
this.getParent+() = endpoint.getBody().asCallableAstNode() and
@@ -166,36 +167,40 @@ private class GrapeHeadersCall extends MethodCall {
// 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 {
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() }
}
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 {
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() }
}
override Http::Server::RequestInputKind getKind() {
result = Http::Server::parameterInputKind()
}
}
/**
/**
* A call to `request` from within a Grape API endpoint.
*/
private class GrapeRequestCall extends MethodCall {
private class GrapeRequestCall extends MethodCall {
GrapeRequestCall() {
exists(GrapeEndpoint endpoint |
this.getParent+() = endpoint.getBody().asCallableAstNode() and
@@ -205,12 +210,12 @@ private class GrapeRequestCall extends MethodCall {
// 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 {
private class GrapeRouteParamCall extends MethodCall {
GrapeRouteParamCall() {
exists(GrapeEndpoint endpoint |
this.getParent+() = endpoint.getBody().asExpr().getExpr() and
@@ -220,13 +225,13 @@ private class GrapeRouteParamCall extends MethodCall {
// 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 {
private class GrapeHeadersBlockCall extends MethodCall {
GrapeHeadersBlockCall() {
exists(GrapeApiClass api |
this.getParent+() = api.getADeclaration() and
@@ -234,13 +239,13 @@ private class GrapeHeadersBlockCall extends MethodCall {
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 {
private class GrapeCookiesBlockCall extends MethodCall {
GrapeCookiesBlockCall() {
exists(GrapeApiClass api |
this.getParent+() = api.getADeclaration() and
@@ -248,13 +253,13 @@ private class GrapeCookiesBlockCall extends MethodCall {
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 {
class GrapeCookiesSource extends Http::Server::RequestInputAccess::Range {
GrapeCookiesSource() {
this.asExpr().getExpr() instanceof GrapeCookiesCall
or
@@ -264,12 +269,12 @@ class GrapeCookiesSource extends Http::Server::RequestInputAccess::Range {
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 {
private class GrapeCookiesCall extends MethodCall {
GrapeCookiesCall() {
exists(GrapeEndpoint endpoint |
this.getParent+() = endpoint.getBody().asCallableAstNode() and
@@ -279,13 +284,13 @@ private class GrapeCookiesCall extends MethodCall {
// 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 class GrapeHelperMethod extends Method {
private GrapeApiClass apiClass;
GrapeHelperMethod() {
@@ -299,16 +304,16 @@ private class GrapeHelperMethod extends Method {
* 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 {
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 |
@@ -318,4 +323,5 @@ private class GrapeHelperMethodTarget extends AdditionalCallTarget {
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() }