Merge pull request #20427 from felickz/ruby-framework-grape

Ruby: Add support for Grape Framework
This commit is contained in:
Tom Hvitved
2025-09-25 16:12:34 +02:00
committed by GitHub
9 changed files with 856 additions and 0 deletions

View File

@@ -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.

View File

@@ -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

View File

@@ -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()
)
}
}
}

View File

@@ -0,0 +1,4 @@
variableIsCaptured
| app.rb:126:9:130:11 | self | CapturedVariable is not captured |
consistencyOverview
| CapturedVariable is not captured | 1 |

View File

@@ -0,0 +1,201 @@
models
edges
| app.rb:103:13:103:18 | call to params | app.rb:103:13:103:70 | call to select | provenance | |
| app.rb:103:13:103:18 | call to params | app.rb:103:13:103:70 | call to select : [collection] [element] | provenance | |
| app.rb:103:13:103:70 | call to select | app.rb:189:21:189:31 | call to user_params | provenance | |
| app.rb:103:13:103:70 | call to select | app.rb:205:21:205:31 | call to user_params | provenance | |
| app.rb:103:13:103:70 | call to select : [collection] [element] | app.rb:189:21:189:31 | call to user_params : [collection] [element] | provenance | |
| app.rb:103:13:103:70 | call to select : [collection] [element] | app.rb:205:21:205:31 | call to user_params : [collection] [element] | provenance | |
| app.rb:107:13:107:32 | call to source | app.rb:183:18:183:43 | call to vulnerable_helper | provenance | |
| app.rb:107:13:107:32 | call to source | app.rb:183:18:183:43 | call to vulnerable_helper | provenance | |
| app.rb:111:13:111:33 | call to source | app.rb:190:25:190:37 | call to simple_helper | provenance | |
| app.rb:111:13:111:33 | call to source | app.rb:190:25:190:37 | call to simple_helper | provenance | |
| app.rb:118:17:118:43 | call to source | app.rb:212:23:212:39 | call to authenticate_user | provenance | |
| app.rb:118:17:118:43 | call to source | app.rb:212:23:212:39 | call to authenticate_user | provenance | |
| app.rb:122:17:122:47 | call to source | app.rb:216:23:216:48 | call to check_permissions | provenance | |
| app.rb:122:17:122:47 | call to source | app.rb:216:23:216:48 | call to check_permissions | provenance | |
| app.rb:128:17:128:42 | call to source | app.rb:220:29:220:80 | call to validate_email | provenance | |
| app.rb:128:17:128:42 | call to source | app.rb:220:29:220:80 | call to validate_email | provenance | |
| app.rb:134:17:134:42 | call to source | app.rb:225:28:225:39 | call to debug_helper | provenance | |
| app.rb:134:17:134:42 | call to source | app.rb:225:28:225:39 | call to debug_helper | provenance | |
| app.rb:140:17:140:37 | call to source | app.rb:230:25:230:37 | call to rescue_helper | provenance | |
| app.rb:140:17:140:37 | call to source | app.rb:230:25:230:37 | call to rescue_helper | provenance | |
| app.rb:150:17:150:35 | call to source | app.rb:235:27:235:37 | call to test_helper | provenance | |
| app.rb:150:17:150:35 | call to source | app.rb:235:27:235:37 | call to test_helper | provenance | |
| app.rb:166:9:166:15 | user_id | app.rb:173:14:173:20 | user_id | provenance | |
| app.rb:166:19:166:24 | call to params | app.rb:166:19:166:34 | ...[...] | provenance | |
| app.rb:166:19:166:34 | ...[...] | app.rb:166:9:166:15 | user_id | provenance | |
| app.rb:167:9:167:16 | route_id | app.rb:174:14:174:21 | route_id | provenance | |
| app.rb:167:20:167:40 | call to route_param | app.rb:167:9:167:16 | route_id | provenance | |
| app.rb:168:9:168:12 | auth | app.rb:175:14:175:17 | auth | provenance | |
| app.rb:168:16:168:22 | call to headers | app.rb:168:16:168:38 | ...[...] | provenance | |
| app.rb:168:16:168:38 | ...[...] | app.rb:168:9:168:12 | auth | provenance | |
| app.rb:169:9:169:15 | session | app.rb:176:14:176:20 | session | provenance | |
| app.rb:169:19:169:25 | call to cookies | app.rb:169:19:169:38 | ...[...] | provenance | |
| app.rb:169:19:169:38 | ...[...] | app.rb:169:9:169:15 | session | provenance | |
| app.rb:183:9:183:14 | result | app.rb:184:14:184:19 | result | provenance | |
| app.rb:183:9:183:14 | result | app.rb:184:14:184:19 | result | provenance | |
| app.rb:183:18:183:43 | call to vulnerable_helper | app.rb:183:9:183:14 | result | provenance | |
| app.rb:183:18:183:43 | call to vulnerable_helper | app.rb:183:9:183:14 | result | provenance | |
| app.rb:189:9:189:17 | user_data | app.rb:191:14:191:22 | user_data | provenance | |
| app.rb:189:9:189:17 | user_data : [collection] [element] | app.rb:191:14:191:22 | user_data | provenance | |
| app.rb:189:21:189:31 | call to user_params | app.rb:189:9:189:17 | user_data | provenance | |
| app.rb:189:21:189:31 | call to user_params : [collection] [element] | app.rb:189:9:189:17 | user_data : [collection] [element] | provenance | |
| app.rb:190:9:190:21 | simple_result | app.rb:192:14:192:26 | simple_result | provenance | |
| app.rb:190:9:190:21 | simple_result | app.rb:192:14:192:26 | simple_result | provenance | |
| app.rb:190:25:190:37 | call to simple_helper | app.rb:190:9:190:21 | simple_result | provenance | |
| app.rb:190:25:190:37 | call to simple_helper | app.rb:190:9:190:21 | simple_result | provenance | |
| app.rb:199:13:199:19 | user_id | app.rb:200:18:200:24 | user_id | provenance | |
| app.rb:199:23:199:28 | call to params | app.rb:199:23:199:33 | ...[...] | provenance | |
| app.rb:199:23:199:33 | ...[...] | app.rb:199:13:199:19 | user_id | provenance | |
| app.rb:205:9:205:17 | user_data | app.rb:206:14:206:22 | user_data | provenance | |
| app.rb:205:9:205:17 | user_data : [collection] [element] | app.rb:206:14:206:22 | user_data | provenance | |
| app.rb:205:21:205:31 | call to user_params | app.rb:205:9:205:17 | user_data | provenance | |
| app.rb:205:21:205:31 | call to user_params : [collection] [element] | app.rb:205:9:205:17 | user_data : [collection] [element] | provenance | |
| app.rb:212:9:212:19 | auth_result | app.rb:213:14:213:24 | auth_result | provenance | |
| app.rb:212:9:212:19 | auth_result | app.rb:213:14:213:24 | auth_result | provenance | |
| app.rb:212:23:212:39 | call to authenticate_user | app.rb:212:9:212:19 | auth_result | provenance | |
| app.rb:212:23:212:39 | call to authenticate_user | app.rb:212:9:212:19 | auth_result | provenance | |
| app.rb:216:9:216:19 | perm_result | app.rb:217:14:217:24 | perm_result | provenance | |
| app.rb:216:9:216:19 | perm_result | app.rb:217:14:217:24 | perm_result | provenance | |
| app.rb:216:23:216:48 | call to check_permissions | app.rb:216:9:216:19 | perm_result | provenance | |
| app.rb:216:23:216:48 | call to check_permissions | app.rb:216:9:216:19 | perm_result | provenance | |
| app.rb:220:9:220:25 | validation_result | app.rb:221:14:221:30 | validation_result | provenance | |
| app.rb:220:9:220:25 | validation_result | app.rb:221:14:221:30 | validation_result | provenance | |
| app.rb:220:29:220:80 | call to validate_email | app.rb:220:9:220:25 | validation_result | provenance | |
| app.rb:220:29:220:80 | call to validate_email | app.rb:220:9:220:25 | validation_result | provenance | |
| app.rb:225:13:225:24 | debug_result | app.rb:226:18:226:29 | debug_result | provenance | |
| app.rb:225:13:225:24 | debug_result | app.rb:226:18:226:29 | debug_result | provenance | |
| app.rb:225:28:225:39 | call to debug_helper | app.rb:225:13:225:24 | debug_result | provenance | |
| app.rb:225:28:225:39 | call to debug_helper | app.rb:225:13:225:24 | debug_result | provenance | |
| app.rb:230:9:230:21 | rescue_result | app.rb:231:14:231:26 | rescue_result | provenance | |
| app.rb:230:9:230:21 | rescue_result | app.rb:231:14:231:26 | rescue_result | provenance | |
| app.rb:230:25:230:37 | call to rescue_helper | app.rb:230:9:230:21 | rescue_result | provenance | |
| app.rb:230:25:230:37 | call to rescue_helper | app.rb:230:9:230:21 | rescue_result | provenance | |
| app.rb:235:13:235:23 | case_result | app.rb:236:18:236:28 | case_result | provenance | |
| app.rb:235:13:235:23 | case_result | app.rb:236:18:236:28 | case_result | provenance | |
| app.rb:235:27:235:37 | call to test_helper | app.rb:235:13:235:23 | case_result | provenance | |
| app.rb:235:27:235:37 | call to test_helper | app.rb:235:13:235:23 | case_result | provenance | |
nodes
| app.rb:103:13:103:18 | call to params | semmle.label | call to params |
| app.rb:103:13:103:70 | call to select | semmle.label | call to select |
| app.rb:103:13:103:70 | call to select : [collection] [element] | semmle.label | call to select : [collection] [element] |
| app.rb:107:13:107:32 | call to source | semmle.label | call to source |
| app.rb:107:13:107:32 | call to source | semmle.label | call to source |
| app.rb:111:13:111:33 | call to source | semmle.label | call to source |
| app.rb:111:13:111:33 | call to source | semmle.label | call to source |
| app.rb:118:17:118:43 | call to source | semmle.label | call to source |
| app.rb:118:17:118:43 | call to source | semmle.label | call to source |
| app.rb:122:17:122:47 | call to source | semmle.label | call to source |
| app.rb:122:17:122:47 | call to source | semmle.label | call to source |
| app.rb:128:17:128:42 | call to source | semmle.label | call to source |
| app.rb:128:17:128:42 | call to source | semmle.label | call to source |
| app.rb:134:17:134:42 | call to source | semmle.label | call to source |
| app.rb:134:17:134:42 | call to source | semmle.label | call to source |
| app.rb:140:17:140:37 | call to source | semmle.label | call to source |
| app.rb:140:17:140:37 | call to source | semmle.label | call to source |
| app.rb:150:17:150:35 | call to source | semmle.label | call to source |
| app.rb:150:17:150:35 | call to source | semmle.label | call to source |
| app.rb:166:9:166:15 | user_id | semmle.label | user_id |
| app.rb:166:19:166:24 | call to params | semmle.label | call to params |
| app.rb:166:19:166:34 | ...[...] | semmle.label | ...[...] |
| app.rb:167:9:167:16 | route_id | semmle.label | route_id |
| app.rb:167:20:167:40 | call to route_param | semmle.label | call to route_param |
| app.rb:168:9:168:12 | auth | semmle.label | auth |
| app.rb:168:16:168:22 | call to headers | semmle.label | call to headers |
| app.rb:168:16:168:38 | ...[...] | semmle.label | ...[...] |
| app.rb:169:9:169:15 | session | semmle.label | session |
| app.rb:169:19:169:25 | call to cookies | semmle.label | call to cookies |
| app.rb:169:19:169:38 | ...[...] | semmle.label | ...[...] |
| app.rb:173:14:173:20 | user_id | semmle.label | user_id |
| app.rb:174:14:174:21 | route_id | semmle.label | route_id |
| app.rb:175:14:175:17 | auth | semmle.label | auth |
| app.rb:176:14:176:20 | session | semmle.label | session |
| app.rb:183:9:183:14 | result | semmle.label | result |
| app.rb:183:9:183:14 | result | semmle.label | result |
| app.rb:183:18:183:43 | call to vulnerable_helper | semmle.label | call to vulnerable_helper |
| app.rb:183:18:183:43 | call to vulnerable_helper | semmle.label | call to vulnerable_helper |
| app.rb:184:14:184:19 | result | semmle.label | result |
| app.rb:184:14:184:19 | result | semmle.label | result |
| app.rb:189:9:189:17 | user_data | semmle.label | user_data |
| app.rb:189:9:189:17 | user_data : [collection] [element] | semmle.label | user_data : [collection] [element] |
| app.rb:189:21:189:31 | call to user_params | semmle.label | call to user_params |
| app.rb:189:21:189:31 | call to user_params : [collection] [element] | semmle.label | call to user_params : [collection] [element] |
| app.rb:190:9:190:21 | simple_result | semmle.label | simple_result |
| app.rb:190:9:190:21 | simple_result | semmle.label | simple_result |
| app.rb:190:25:190:37 | call to simple_helper | semmle.label | call to simple_helper |
| app.rb:190:25:190:37 | call to simple_helper | semmle.label | call to simple_helper |
| app.rb:191:14:191:22 | user_data | semmle.label | user_data |
| app.rb:192:14:192:26 | simple_result | semmle.label | simple_result |
| app.rb:192:14:192:26 | simple_result | semmle.label | simple_result |
| app.rb:199:13:199:19 | user_id | semmle.label | user_id |
| app.rb:199:23:199:28 | call to params | semmle.label | call to params |
| app.rb:199:23:199:33 | ...[...] | semmle.label | ...[...] |
| app.rb:200:18:200:24 | user_id | semmle.label | user_id |
| app.rb:205:9:205:17 | user_data | semmle.label | user_data |
| app.rb:205:9:205:17 | user_data : [collection] [element] | semmle.label | user_data : [collection] [element] |
| app.rb:205:21:205:31 | call to user_params | semmle.label | call to user_params |
| app.rb:205:21:205:31 | call to user_params : [collection] [element] | semmle.label | call to user_params : [collection] [element] |
| app.rb:206:14:206:22 | user_data | semmle.label | user_data |
| app.rb:212:9:212:19 | auth_result | semmle.label | auth_result |
| app.rb:212:9:212:19 | auth_result | semmle.label | auth_result |
| app.rb:212:23:212:39 | call to authenticate_user | semmle.label | call to authenticate_user |
| app.rb:212:23:212:39 | call to authenticate_user | semmle.label | call to authenticate_user |
| app.rb:213:14:213:24 | auth_result | semmle.label | auth_result |
| app.rb:213:14:213:24 | auth_result | semmle.label | auth_result |
| app.rb:216:9:216:19 | perm_result | semmle.label | perm_result |
| app.rb:216:9:216:19 | perm_result | semmle.label | perm_result |
| app.rb:216:23:216:48 | call to check_permissions | semmle.label | call to check_permissions |
| app.rb:216:23:216:48 | call to check_permissions | semmle.label | call to check_permissions |
| app.rb:217:14:217:24 | perm_result | semmle.label | perm_result |
| app.rb:217:14:217:24 | perm_result | semmle.label | perm_result |
| app.rb:220:9:220:25 | validation_result | semmle.label | validation_result |
| app.rb:220:9:220:25 | validation_result | semmle.label | validation_result |
| app.rb:220:29:220:80 | call to validate_email | semmle.label | call to validate_email |
| app.rb:220:29:220:80 | call to validate_email | semmle.label | call to validate_email |
| app.rb:221:14:221:30 | validation_result | semmle.label | validation_result |
| app.rb:221:14:221:30 | validation_result | semmle.label | validation_result |
| app.rb:225:13:225:24 | debug_result | semmle.label | debug_result |
| app.rb:225:13:225:24 | debug_result | semmle.label | debug_result |
| app.rb:225:28:225:39 | call to debug_helper | semmle.label | call to debug_helper |
| app.rb:225:28:225:39 | call to debug_helper | semmle.label | call to debug_helper |
| app.rb:226:18:226:29 | debug_result | semmle.label | debug_result |
| app.rb:226:18:226:29 | debug_result | semmle.label | debug_result |
| app.rb:230:9:230:21 | rescue_result | semmle.label | rescue_result |
| app.rb:230:9:230:21 | rescue_result | semmle.label | rescue_result |
| app.rb:230:25:230:37 | call to rescue_helper | semmle.label | call to rescue_helper |
| app.rb:230:25:230:37 | call to rescue_helper | semmle.label | call to rescue_helper |
| app.rb:231:14:231:26 | rescue_result | semmle.label | rescue_result |
| app.rb:231:14:231:26 | rescue_result | semmle.label | rescue_result |
| app.rb:235:13:235:23 | case_result | semmle.label | case_result |
| app.rb:235:13:235:23 | case_result | semmle.label | case_result |
| app.rb:235:27:235:37 | call to test_helper | semmle.label | call to test_helper |
| app.rb:235:27:235:37 | call to test_helper | semmle.label | call to test_helper |
| app.rb:236:18:236:28 | case_result | semmle.label | case_result |
| app.rb:236:18:236:28 | case_result | semmle.label | case_result |
subpaths
testFailures
#select
| app.rb:173:14:173:20 | user_id | app.rb:166:19:166:24 | call to params | app.rb:173:14:173:20 | user_id | $@ | app.rb:166:19:166:24 | call to params | call to params |
| app.rb:174:14:174:21 | route_id | app.rb:167:20:167:40 | call to route_param | app.rb:174:14:174:21 | route_id | $@ | app.rb:167:20:167:40 | call to route_param | call to route_param |
| app.rb:175:14:175:17 | auth | app.rb:168:16:168:22 | call to headers | app.rb:175:14:175:17 | auth | $@ | app.rb:168:16:168:22 | call to headers | call to headers |
| app.rb:176:14:176:20 | session | app.rb:169:19:169:25 | call to cookies | app.rb:176:14:176:20 | session | $@ | app.rb:169:19:169:25 | call to cookies | call to cookies |
| app.rb:184:14:184:19 | result | app.rb:107:13:107:32 | call to source | app.rb:184:14:184:19 | result | $@ | app.rb:107:13:107:32 | call to source | call to source |
| app.rb:184:14:184:19 | result | app.rb:107:13:107:32 | call to source | app.rb:184:14:184:19 | result | $@ | app.rb:107:13:107:32 | call to source | call to source |
| app.rb:191:14:191:22 | user_data | app.rb:103:13:103:18 | call to params | app.rb:191:14:191:22 | user_data | $@ | app.rb:103:13:103:18 | call to params | call to params |
| app.rb:192:14:192:26 | simple_result | app.rb:111:13:111:33 | call to source | app.rb:192:14:192:26 | simple_result | $@ | app.rb:111:13:111:33 | call to source | call to source |
| app.rb:192:14:192:26 | simple_result | app.rb:111:13:111:33 | call to source | app.rb:192:14:192:26 | simple_result | $@ | app.rb:111:13:111:33 | call to source | call to source |
| app.rb:200:18:200:24 | user_id | app.rb:199:23:199:28 | call to params | app.rb:200:18:200:24 | user_id | $@ | app.rb:199:23:199:28 | call to params | call to params |
| app.rb:206:14:206:22 | user_data | app.rb:103:13:103:18 | call to params | app.rb:206:14:206:22 | user_data | $@ | app.rb:103:13:103:18 | call to params | call to params |
| app.rb:213:14:213:24 | auth_result | app.rb:118:17:118:43 | call to source | app.rb:213:14:213:24 | auth_result | $@ | app.rb:118:17:118:43 | call to source | call to source |
| app.rb:213:14:213:24 | auth_result | app.rb:118:17:118:43 | call to source | app.rb:213:14:213:24 | auth_result | $@ | app.rb:118:17:118:43 | call to source | call to source |
| app.rb:217:14:217:24 | perm_result | app.rb:122:17:122:47 | call to source | app.rb:217:14:217:24 | perm_result | $@ | app.rb:122:17:122:47 | call to source | call to source |
| app.rb:217:14:217:24 | perm_result | app.rb:122:17:122:47 | call to source | app.rb:217:14:217:24 | perm_result | $@ | app.rb:122:17:122:47 | call to source | call to source |
| app.rb:221:14:221:30 | validation_result | app.rb:128:17:128:42 | call to source | app.rb:221:14:221:30 | validation_result | $@ | app.rb:128:17:128:42 | call to source | call to source |
| app.rb:221:14:221:30 | validation_result | app.rb:128:17:128:42 | call to source | app.rb:221:14:221:30 | validation_result | $@ | app.rb:128:17:128:42 | call to source | call to source |
| app.rb:226:18:226:29 | debug_result | app.rb:134:17:134:42 | call to source | app.rb:226:18:226:29 | debug_result | $@ | app.rb:134:17:134:42 | call to source | call to source |
| app.rb:226:18:226:29 | debug_result | app.rb:134:17:134:42 | call to source | app.rb:226:18:226:29 | debug_result | $@ | app.rb:134:17:134:42 | call to source | call to source |
| app.rb:231:14:231:26 | rescue_result | app.rb:140:17:140:37 | call to source | app.rb:231:14:231:26 | rescue_result | $@ | app.rb:140:17:140:37 | call to source | call to source |
| app.rb:231:14:231:26 | rescue_result | app.rb:140:17:140:37 | call to source | app.rb:231:14:231:26 | rescue_result | $@ | app.rb:140:17:140:37 | call to source | call to source |
| app.rb:236:18:236:28 | case_result | app.rb:150:17:150:35 | call to source | app.rb:236:18:236:28 | case_result | $@ | app.rb:150:17:150:35 | call to source | call to source |
| app.rb:236:18:236:28 | case_result | app.rb:150:17:150:35 | call to source | app.rb:236:18:236:28 | case_result | $@ | app.rb:150:17:150:35 | call to source | call to source |

View File

@@ -0,0 +1,25 @@
/**
* @kind path-problem
*/
import ruby
import utils.test.InlineFlowTest
import PathGraph
import codeql.ruby.frameworks.Grape
import codeql.ruby.Concepts
module GrapeConfig implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) {
source instanceof Http::Server::RequestInputAccess::Range
or
DefaultFlowConfig::isSource(source)
}
predicate isSink(DataFlow::Node sink) { DefaultFlowConfig::isSink(sink) }
}
import FlowTest<DefaultFlowConfig, GrapeConfig>
from PathNode source, PathNode sink
where flowPath(source, sink)
select sink, source, sink, "$@", source, source.toString()

View File

@@ -0,0 +1,58 @@
grapeApiClasses
| app.rb:1:1:90:3 | MyAPI |
| app.rb:92:1:96:3 | AdminAPI |
| app.rb:98:1:239:3 | UserAPI |
grapeEndpoints
| app.rb:1:1:90:3 | MyAPI | app.rb:7:3:11:5 | call to get | GET | /hello/:name |
| app.rb:1:1:90:3 | MyAPI | app.rb:17:3:20:5 | call to post | POST | /messages |
| app.rb:1:1:90:3 | MyAPI | app.rb:23:3:27:5 | call to put | PUT | /update/:id |
| app.rb:1:1:90:3 | MyAPI | app.rb:30:3:32:5 | call to delete | DELETE | /items/:id |
| app.rb:1:1:90:3 | MyAPI | app.rb:35:3:37:5 | call to patch | PATCH | /items/:id |
| app.rb:1:1:90:3 | MyAPI | app.rb:40:3:42:5 | call to head | HEAD | /status |
| app.rb:1:1:90:3 | MyAPI | app.rb:45:3:47:5 | call to options | OPTIONS | /info |
| app.rb:1:1:90:3 | MyAPI | app.rb:50:3:54:5 | call to get | GET | /users/:user_id/posts/:post_id |
| app.rb:1:1:90:3 | MyAPI | app.rb:78:3:82:5 | call to get | GET | /cookie_test |
| app.rb:1:1:90:3 | MyAPI | app.rb:85:3:89:5 | call to get | GET | /header_test |
| app.rb:92:1:96:3 | AdminAPI | app.rb:93:3:95:5 | call to get | GET | /admin |
| app.rb:98:1:239:3 | UserAPI | app.rb:164:5:178:7 | call to get | GET | /comprehensive_test/:user_id |
| app.rb:98:1:239:3 | UserAPI | app.rb:180:5:185:7 | call to get | GET | /helper_test/:user_id |
| app.rb:98:1:239:3 | UserAPI | app.rb:187:5:193:7 | call to post | POST | /users |
| app.rb:98:1:239:3 | UserAPI | app.rb:204:5:207:7 | call to post | POST | /users |
| app.rb:98:1:239:3 | UserAPI | app.rb:210:5:238:7 | call to get | GET | /nested_test/:token |
grapeParams
| app.rb:8:12:8:17 | call to params |
| app.rb:14:3:16:5 | call to params |
| app.rb:18:11:18:16 | call to params |
| app.rb:24:10:24:15 | call to params |
| app.rb:31:5:31:10 | call to params |
| app.rb:36:5:36:10 | call to params |
| app.rb:60:12:60:17 | call to params |
| app.rb:94:5:94:10 | call to params |
| app.rb:103:13:103:18 | call to params |
| app.rb:117:25:117:30 | call to params |
| app.rb:166:19:166:24 | call to params |
| app.rb:182:19:182:24 | call to params |
| app.rb:199:23:199:28 | call to params |
grapeHeaders
| app.rb:9:18:9:24 | call to headers |
| app.rb:46:5:46:11 | call to headers |
| app.rb:66:3:69:5 | call to headers |
| app.rb:86:12:86:18 | call to headers |
| app.rb:87:14:87:20 | call to headers |
| app.rb:156:5:158:7 | call to headers |
| app.rb:168:16:168:22 | call to headers |
grapeRequest
| app.rb:25:12:25:18 | call to request |
| app.rb:170:21:170:27 | call to request |
grapeRouteParam
| app.rb:51:15:51:35 | call to route_param |
| app.rb:52:15:52:36 | call to route_param |
| app.rb:57:3:63:5 | call to route_param |
| app.rb:167:20:167:40 | call to route_param |
| app.rb:196:5:202:7 | call to route_param |
grapeCookies
| app.rb:72:3:75:5 | call to cookies |
| app.rb:79:15:79:21 | call to cookies |
| app.rb:80:16:80:22 | call to cookies |
| app.rb:160:5:162:7 | call to cookies |
| app.rb:169:19:169:25 | call to cookies |

View File

@@ -0,0 +1,24 @@
import ruby
import codeql.ruby.frameworks.Grape
import codeql.ruby.Concepts
import codeql.ruby.AST
query predicate grapeApiClasses(Grape::GrapeApiClass api) { any() }
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(Grape::GrapeParamsSource params) { any() }
query predicate grapeHeaders(Grape::GrapeHeadersSource headers) { any() }
query predicate grapeRequest(Grape::GrapeRequestSource request) { any() }
query predicate grapeRouteParam(Grape::GrapeRouteParamSource routeParam) { any() }
query predicate grapeCookies(Grape::GrapeCookiesSource cookies) { any() }

View File

@@ -0,0 +1,239 @@
class MyAPI < Grape::API
version 'v1', using: :header, vendor: 'myapi'
format :json
prefix :api
desc 'Simple get endpoint'
get '/hello/:name' do
name = params[:name]
user_agent = headers['User-Agent']
"Hello #{name}!"
end
desc 'Post endpoint with params'
params do
requires :message, type: String
end
post '/messages' do
msg = params[:message]
{ status: 'received', message: msg }
end
desc 'Put endpoint accessing request'
put '/update/:id' do
id = params[:id]
body = request.body.read
{ id: id, body: body }
end
desc 'Delete endpoint'
delete '/items/:id' do
params[:id]
end
desc 'Patch endpoint'
patch '/items/:id' do
params[:id]
end
desc 'Head endpoint'
head '/status' do
# Just return status
end
desc 'Options endpoint'
options '/info' do
headers['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS'
end
desc 'Route param endpoint'
get '/users/:user_id/posts/:post_id' do
user_id = route_param(:user_id)
post_id = route_param('post_id')
{ user_id: user_id, post_id: post_id }
end
desc 'Route param block pattern'
route_param :id do
get do
# params[:id] is user input from the path parameter
id = params[:id]
{ id: id }
end
end
# Headers block for defining expected headers
headers do
requires :Authorization, type: String
optional 'X-Custom-Header', type: String
end
# Cookies block for defining expected cookies
cookies do
requires :session_id, type: String
optional :tracking_id, type: String
end
desc 'Endpoint that uses cookies method'
get '/cookie_test' do
session = cookies[:session_id]
tracking = cookies['tracking_id']
{ session: session, tracking: tracking }
end
desc 'Endpoint that uses headers method'
get '/header_test' do
auth = headers[:Authorization]
custom = headers['X-Custom-Header']
{ auth: auth, custom: custom }
end
end
class AdminAPI < Grape::API
get '/admin' do
params[:token]
end
end
class UserAPI < Grape::API
VALID_PARAMS = %w(name email password password_confirmation)
helpers do
def user_params
params.select{|key,value| VALID_PARAMS.include?(key.to_s)} # Real helper implementation
end
def vulnerable_helper(user_id)
source "paramHelper" # Test parameter passing to helper
end
def simple_helper
source "simpleHelper" # Test simple helper return
end
# Nested helper scenarios that require getParent+()
module AuthHelpers
def authenticate_user
token = params[:token]
source "nestedModuleHelper" # Test nested module helper
end
def check_permissions(resource)
source "nestedPermissionHelper" # Test nested module helper with params
end
end
class ValidationHelpers
def self.validate_email(email)
source "nestedClassHelper" # Test nested class helper
end
end
if Rails.env.development?
def debug_helper
source "conditionalHelper" # Test helper inside conditional block
end
end
begin
def rescue_helper
source "rescueHelper" # Test helper inside begin block
end
rescue
# error handling
end
# Helper inside a case statement
case ENV['RACK_ENV']
when 'test'
def test_helper
source "caseHelper" # Test helper inside case block
end
end
end
# Headers and cookies blocks for DSL testing
headers do
requires :Authorization, type: String
end
cookies do
requires :session_id, type: String
end
get '/comprehensive_test/:user_id' do
# Test all Grape input sources
user_id = params[:user_id] # params taint source
route_id = route_param(:user_id) # route_param taint source
auth = headers[:Authorization] # headers taint source
session = cookies[:session_id] # cookies taint source
body_data = request.body.read # request taint source
# Test sinks for all sources
sink user_id # $ hasTaintFlow
sink route_id # $ hasTaintFlow
sink auth # $ hasTaintFlow
sink session # $ hasTaintFlow
# Note: request.body.read may not be detected by this flow test config
end
get '/helper_test/:user_id' do
# Test helper method parameter passing dataflow
user_id = params[:user_id]
result = vulnerable_helper(user_id)
sink result # $ hasValueFlow=paramHelper
end
post '/users' do
# Test helper method return dataflow
user_data = user_params
simple_result = simple_helper
sink user_data # $ hasTaintFlow
sink simple_result # $ hasValueFlow=simpleHelper
end
# Test route_param block pattern
route_param :id do
get do
# params[:id] should be user input from the path
user_id = params[:id]
sink user_id # $ hasTaintFlow
end
end
post '/users' do
user_data = user_params
sink user_data # $ hasTaintFlow
end
# Test nested helper methods
get '/nested_test/:token' do
# Test nested module helper
auth_result = authenticate_user
sink auth_result # $ hasValueFlow=nestedModuleHelper
# Test nested module helper with parameters
perm_result = check_permissions("admin")
sink perm_result # $ hasValueFlow=nestedPermissionHelper
# Test nested class helper
validation_result = ValidationHelpers.validate_email("test@example.com")
sink validation_result # $ hasValueFlow=nestedClassHelper
# Test conditional helper (if it exists)
if respond_to?(:debug_helper)
debug_result = debug_helper
sink debug_result # $ hasValueFlow=conditionalHelper
end
# Test rescue helper
rescue_result = rescue_helper
sink rescue_result # $ hasValueFlow=rescueHelper
# Test case helper (if it exists)
if respond_to?(:test_helper)
case_result = test_helper
sink case_result # $ hasValueFlow=caseHelper
end
end
end