From f4bbbc346fe5b270f6fbfc3ed351fdfdaee3fa46 Mon Sep 17 00:00:00 2001 From: Chad Bentz <1760475+felickz@users.noreply.github.com> Date: Fri, 19 Sep 2025 19:06:50 -0400 Subject: [PATCH] Refactor Grape framework to be encapsulated properly in Module --- ruby/ql/lib/codeql/ruby/frameworks/Grape.qll | 550 +++++++++--------- .../library-tests/frameworks/grape/Grape.ql | 14 +- 2 files changed, 285 insertions(+), 279 deletions(-) diff --git a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll index 31632e01948..0999be94505 100644 --- a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll +++ b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll @@ -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() + ) + } } } diff --git a/ruby/ql/test/library-tests/frameworks/grape/Grape.ql b/ruby/ql/test/library-tests/frameworks/grape/Grape.ql index c9aa7c29082..c5f0798f7a6 100644 --- a/ruby/ql/test/library-tests/frameworks/grape/Grape.ql +++ b/ruby/ql/test/library-tests/frameworks/grape/Grape.ql @@ -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() }