From d295acc3c33268fe10fa5aaf20fde43ef900c59d Mon Sep 17 00:00:00 2001 From: Chad Bentz <1760475+felickz@users.noreply.github.com> Date: Fri, 12 Sep 2025 19:22:05 -0400 Subject: [PATCH 01/19] Add initial support for Ruby Grape --- ruby/ql/lib/codeql/ruby/Frameworks.qll | 1 + ruby/ql/lib/codeql/ruby/frameworks/Grape.qll | 198 ++++++++++++++++++ .../frameworks/grape/Grape.expected | 25 +++ .../library-tests/frameworks/grape/Grape.ql | 18 ++ .../library-tests/frameworks/grape/app.rb | 54 +++++ .../security/cwe-089/ArelInjection.rb | 9 + .../security/cwe-089/SqlInjection.expected | 11 + 7 files changed, 316 insertions(+) create mode 100644 ruby/ql/lib/codeql/ruby/frameworks/Grape.qll create mode 100644 ruby/ql/test/library-tests/frameworks/grape/Grape.expected create mode 100644 ruby/ql/test/library-tests/frameworks/grape/Grape.ql create mode 100644 ruby/ql/test/library-tests/frameworks/grape/app.rb diff --git a/ruby/ql/lib/codeql/ruby/Frameworks.qll b/ruby/ql/lib/codeql/ruby/Frameworks.qll index 9bc01874710d..e8009c91b7d1 100644 --- a/ruby/ql/lib/codeql/ruby/Frameworks.qll +++ b/ruby/ql/lib/codeql/ruby/Frameworks.qll @@ -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 diff --git a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll new file mode 100644 index 000000000000..8e9a062dc9a6 --- /dev/null +++ b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll @@ -0,0 +1,198 @@ +/** + * 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 + +/** + * 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. + */ +private class GrapeParamsCall extends ParamsCallImpl { + GrapeParamsCall() { + exists(GrapeEndpoint endpoint | + this.getParent+() = endpoint.getBody().asCallableAstNode() and + this.getMethodName() = "params" + ) + or + // Also handle cases where params is called on an instance of a Grape API class + this = grapeAPIInstance().getAMethodCall("params").asExpr().getExpr() + } +} + +/** + * A call to `headers` from within a Grape API endpoint. + * Headers can also be a source of user input. + */ +class GrapeHeadersSource extends Http::Server::RequestInputAccess::Range { + GrapeHeadersSource() { + this.asExpr().getExpr() instanceof GrapeHeadersCall + } + + 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 `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() + } +} \ No newline at end of file diff --git a/ruby/ql/test/library-tests/frameworks/grape/Grape.expected b/ruby/ql/test/library-tests/frameworks/grape/Grape.expected new file mode 100644 index 000000000000..904cb36333a4 --- /dev/null +++ b/ruby/ql/test/library-tests/frameworks/grape/Grape.expected @@ -0,0 +1,25 @@ +grapeAPIClasses +| app.rb:1:1:48:3 | MyAPI | +| app.rb:50:1:54:3 | AdminAPI | +grapeEndpoints +| app.rb:1:1:48:3 | MyAPI | app.rb:7:3:11:5 | call to get | GET | /hello/:name | +| app.rb:1:1:48:3 | MyAPI | app.rb:17:3:20:5 | call to post | POST | /messages | +| app.rb:1:1:48:3 | MyAPI | app.rb:23:3:27:5 | call to put | PUT | /update/:id | +| app.rb:1:1:48:3 | MyAPI | app.rb:30:3:32:5 | call to delete | DELETE | /items/:id | +| app.rb:1:1:48:3 | MyAPI | app.rb:35:3:37:5 | call to patch | PATCH | /items/:id | +| app.rb:1:1:48:3 | MyAPI | app.rb:40:3:42:5 | call to head | HEAD | /status | +| app.rb:1:1:48:3 | MyAPI | app.rb:45:3:47:5 | call to options | OPTIONS | /info | +| app.rb:50:1:54:3 | AdminAPI | app.rb:51:3:53:5 | call to get | GET | /admin | +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:52:5:52:10 | call to params | +grapeHeaders +| app.rb:9:18:9:24 | call to headers | +| app.rb:46:5:46:11 | call to headers | +grapeRequest +| app.rb:25:12:25:18 | call to request | \ No newline at end of file diff --git a/ruby/ql/test/library-tests/frameworks/grape/Grape.ql b/ruby/ql/test/library-tests/frameworks/grape/Grape.ql new file mode 100644 index 000000000000..a35c639d9ad8 --- /dev/null +++ b/ruby/ql/test/library-tests/frameworks/grape/Grape.ql @@ -0,0 +1,18 @@ +import ruby +import codeql.ruby.frameworks.Grape +import codeql.ruby.Concepts +import codeql.ruby.AST + +query predicate grapeAPIClasses(GrapeAPIClass api) { any() } + +query predicate grapeEndpoints(GrapeAPIClass api, 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 grapeHeaders(GrapeHeadersSource headers) { any() } + +query predicate grapeRequest(GrapeRequestSource request) { any() } \ No newline at end of file diff --git a/ruby/ql/test/library-tests/frameworks/grape/app.rb b/ruby/ql/test/library-tests/frameworks/grape/app.rb new file mode 100644 index 000000000000..3e33caa85e91 --- /dev/null +++ b/ruby/ql/test/library-tests/frameworks/grape/app.rb @@ -0,0 +1,54 @@ +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 +end + +class AdminAPI < Grape::API + get '/admin' do + params[:token] + end +end \ No newline at end of file diff --git a/ruby/ql/test/query-tests/security/cwe-089/ArelInjection.rb b/ruby/ql/test/query-tests/security/cwe-089/ArelInjection.rb index 1cd6782b2416..30832894b9e4 100644 --- a/ruby/ql/test/query-tests/security/cwe-089/ArelInjection.rb +++ b/ruby/ql/test/query-tests/security/cwe-089/ArelInjection.rb @@ -6,4 +6,13 @@ def unsafe_action sql = Arel.sql("SELECT * FROM users WHERE name = #{name}") sql = Arel::Nodes::SqlLiteral.new("SELECT * FROM users WHERE name = #{name}") end +end + +class PotatoAPI < Grape::API + get '/unsafe_endpoint' do + name = params[:user_name] + # BAD: SQL statement constructed from user input + sql = Arel.sql("SELECT * FROM users WHERE name = #{name}") + sql = Arel::Nodes::SqlLiteral.new("SELECT * FROM users WHERE name = #{name}") + end end \ No newline at end of file diff --git a/ruby/ql/test/query-tests/security/cwe-089/SqlInjection.expected b/ruby/ql/test/query-tests/security/cwe-089/SqlInjection.expected index 069cb34810fc..b8b1350882d8 100644 --- a/ruby/ql/test/query-tests/security/cwe-089/SqlInjection.expected +++ b/ruby/ql/test/query-tests/security/cwe-089/SqlInjection.expected @@ -81,6 +81,10 @@ edges | ArelInjection.rb:4:5:4:8 | name | ArelInjection.rb:7:39:7:80 | "SELECT * FROM users WHERE nam..." | provenance | AdditionalTaintStep | | ArelInjection.rb:4:12:4:17 | call to params | ArelInjection.rb:4:12:4:29 | ...[...] | provenance | | | ArelInjection.rb:4:12:4:29 | ...[...] | ArelInjection.rb:4:5:4:8 | name | provenance | | +| ArelInjection.rb:13:5:13:8 | name | ArelInjection.rb:15:20:15:61 | "SELECT * FROM users WHERE nam..." | provenance | AdditionalTaintStep | +| ArelInjection.rb:13:5:13:8 | name | ArelInjection.rb:16:39:16:80 | "SELECT * FROM users WHERE nam..." | provenance | AdditionalTaintStep | +| ArelInjection.rb:13:12:13:17 | call to params | ArelInjection.rb:13:12:13:29 | ...[...] | provenance | | +| ArelInjection.rb:13:12:13:29 | ...[...] | ArelInjection.rb:13:5:13:8 | name | provenance | | | PgInjection.rb:6:5:6:8 | name | PgInjection.rb:13:5:13:8 | qry1 : String | provenance | AdditionalTaintStep | | PgInjection.rb:6:5:6:8 | name | PgInjection.rb:19:5:19:8 | qry2 : String | provenance | AdditionalTaintStep | | PgInjection.rb:6:5:6:8 | name | PgInjection.rb:31:5:31:8 | qry3 : String | provenance | AdditionalTaintStep | @@ -209,6 +213,11 @@ nodes | ArelInjection.rb:4:12:4:29 | ...[...] | semmle.label | ...[...] | | ArelInjection.rb:6:20:6:61 | "SELECT * FROM users WHERE nam..." | semmle.label | "SELECT * FROM users WHERE nam..." | | ArelInjection.rb:7:39:7:80 | "SELECT * FROM users WHERE nam..." | semmle.label | "SELECT * FROM users WHERE nam..." | +| ArelInjection.rb:13:5:13:8 | name | semmle.label | name | +| ArelInjection.rb:13:12:13:17 | call to params | semmle.label | call to params | +| ArelInjection.rb:13:12:13:29 | ...[...] | semmle.label | ...[...] | +| ArelInjection.rb:15:20:15:61 | "SELECT * FROM users WHERE nam..." | semmle.label | "SELECT * FROM users WHERE nam..." | +| ArelInjection.rb:16:39:16:80 | "SELECT * FROM users WHERE nam..." | semmle.label | "SELECT * FROM users WHERE nam..." | | PgInjection.rb:6:5:6:8 | name | semmle.label | name | | PgInjection.rb:6:12:6:17 | call to params | semmle.label | call to params | | PgInjection.rb:6:12:6:24 | ...[...] | semmle.label | ...[...] | @@ -266,6 +275,8 @@ subpaths | ActiveRecordInjection.rb:216:38:216:53 | "role = #{...}" | ActiveRecordInjection.rb:222:29:222:34 | call to params | ActiveRecordInjection.rb:216:38:216:53 | "role = #{...}" | This SQL query depends on a $@. | ActiveRecordInjection.rb:222:29:222:34 | call to params | user-provided value | | ArelInjection.rb:6:20:6:61 | "SELECT * FROM users WHERE nam..." | ArelInjection.rb:4:12:4:17 | call to params | ArelInjection.rb:6:20:6:61 | "SELECT * FROM users WHERE nam..." | This SQL query depends on a $@. | ArelInjection.rb:4:12:4:17 | call to params | user-provided value | | ArelInjection.rb:7:39:7:80 | "SELECT * FROM users WHERE nam..." | ArelInjection.rb:4:12:4:17 | call to params | ArelInjection.rb:7:39:7:80 | "SELECT * FROM users WHERE nam..." | This SQL query depends on a $@. | ArelInjection.rb:4:12:4:17 | call to params | user-provided value | +| ArelInjection.rb:15:20:15:61 | "SELECT * FROM users WHERE nam..." | ArelInjection.rb:13:12:13:17 | call to params | ArelInjection.rb:15:20:15:61 | "SELECT * FROM users WHERE nam..." | This SQL query depends on a $@. | ArelInjection.rb:13:12:13:17 | call to params | user-provided value | +| ArelInjection.rb:16:39:16:80 | "SELECT * FROM users WHERE nam..." | ArelInjection.rb:13:12:13:17 | call to params | ArelInjection.rb:16:39:16:80 | "SELECT * FROM users WHERE nam..." | This SQL query depends on a $@. | ArelInjection.rb:13:12:13:17 | call to params | user-provided value | | PgInjection.rb:14:15:14:18 | qry1 | PgInjection.rb:6:12:6:17 | call to params | PgInjection.rb:14:15:14:18 | qry1 | This SQL query depends on a $@. | PgInjection.rb:6:12:6:17 | call to params | user-provided value | | PgInjection.rb:15:21:15:24 | qry1 | PgInjection.rb:6:12:6:17 | call to params | PgInjection.rb:15:21:15:24 | qry1 | This SQL query depends on a $@. | PgInjection.rb:6:12:6:17 | call to params | user-provided value | | PgInjection.rb:20:22:20:25 | qry2 | PgInjection.rb:6:12:6:17 | call to params | PgInjection.rb:20:22:20:25 | qry2 | This SQL query depends on a $@. | PgInjection.rb:6:12:6:17 | call to params | user-provided value | From 738ab6fba7ff64b97c60cf7f2593f136c5bf0f04 Mon Sep 17 00:00:00 2001 From: Chad Bentz <1760475+felickz@users.noreply.github.com> Date: Fri, 12 Sep 2025 19:23:15 -0400 Subject: [PATCH 02/19] Refactor Grape framework code for improved readability and consistency --- ruby/ql/lib/codeql/ruby/frameworks/Grape.qll | 2 +- .../test/library-tests/frameworks/grape/Grape.ql | 2 +- .../test/library-tests/frameworks/grape/app.rb | 16 ++++++++-------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll index 8e9a062dc9a6..857b849f4258 100644 --- a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll +++ b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll @@ -115,7 +115,7 @@ class GrapeEndpoint extends DataFlow::CallNode { * Grape parameters available via the `params` method within an endpoint. */ class GrapeParamsSource extends Http::Server::RequestInputAccess::Range { - GrapeParamsSource() { + GrapeParamsSource() { this.asExpr().getExpr() instanceof GrapeParamsCall } diff --git a/ruby/ql/test/library-tests/frameworks/grape/Grape.ql b/ruby/ql/test/library-tests/frameworks/grape/Grape.ql index a35c639d9ad8..3dd7c488a497 100644 --- a/ruby/ql/test/library-tests/frameworks/grape/Grape.ql +++ b/ruby/ql/test/library-tests/frameworks/grape/Grape.ql @@ -5,7 +5,7 @@ import codeql.ruby.AST query predicate grapeAPIClasses(GrapeAPIClass api) { any() } -query predicate grapeEndpoints(GrapeAPIClass api, GrapeEndpoint endpoint, string method, string path) { +query predicate grapeEndpoints(GrapeAPIClass api, GrapeEndpoint endpoint, string method, string path) { endpoint = api.getAnEndpoint() and method = endpoint.getHttpMethod() and path = endpoint.getPath() diff --git a/ruby/ql/test/library-tests/frameworks/grape/app.rb b/ruby/ql/test/library-tests/frameworks/grape/app.rb index 3e33caa85e91..6333240debe8 100644 --- a/ruby/ql/test/library-tests/frameworks/grape/app.rb +++ b/ruby/ql/test/library-tests/frameworks/grape/app.rb @@ -9,7 +9,7 @@ class MyAPI < Grape::API user_agent = headers['User-Agent'] "Hello #{name}!" end - + desc 'Post endpoint with params' params do requires :message, type: String @@ -18,36 +18,36 @@ class MyAPI < Grape::API 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' + + 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 end -class AdminAPI < Grape::API +class AdminAPI < Grape::API get '/admin' do params[:token] end From 3252bd39d2e671710e94ada3adaae1cefb1deaf1 Mon Sep 17 00:00:00 2001 From: Chad Bentz <1760475+felickz@users.noreply.github.com> Date: Fri, 12 Sep 2025 22:13:21 -0400 Subject: [PATCH 03/19] Enhance Grape framework with additional data flow modeling and helper method support --- ruby/ql/lib/codeql/ruby/frameworks/Grape.qll | 57 +++++++++++++++---- .../frameworks/grape/Grape.expected | 2 +- .../security/cwe-089/ArelInjection.rb | 25 +++++++- .../security/cwe-089/SqlInjection.expected | 20 +++++++ 4 files changed, 92 insertions(+), 12 deletions(-) diff --git a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll index 857b849f4258..a3aa2f684c79 100644 --- a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll +++ b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll @@ -12,6 +12,7 @@ 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. @@ -125,21 +126,17 @@ class GrapeParamsSource extends Http::Server::RequestInputAccess::Range { } /** - * A call to `params` from within a Grape API endpoint. + * A call to `params` from within a Grape API endpoint or helper method. */ private class GrapeParamsCall extends ParamsCallImpl { GrapeParamsCall() { - exists(GrapeEndpoint endpoint | - this.getParent+() = endpoint.getBody().asCallableAstNode() and - this.getMethodName() = "params" + // Simplified approach: find params calls that are descendants of Grape API class methods + exists(GrapeAPIClass api | + this.getMethodName() = "params" and + this.getParent+() = api.getADeclaration() ) - or - // Also handle cases where params is called on an instance of a Grape API class - this = grapeAPIInstance().getAMethodCall("params").asExpr().getExpr() } -} - -/** +}/** * A call to `headers` from within a Grape API endpoint. * Headers can also be a source of user input. */ @@ -195,4 +192,44 @@ 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 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 + * 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) { + exists(GrapeHelperMethod helperMethod, MethodCall call, int i | + // Find calls to helper methods from within Grape endpoints + call.getMethodName() = helperMethod.getName() and + exists(GrapeEndpoint endpoint | + call.getParent+() = endpoint.getBody().asExpr().getExpr() + ) and + // Map argument to parameter + nodeFrom.asExpr().getExpr() = call.getArgument(i) and + nodeTo.asParameter() = helperMethod.getParameter(i) + ) + } } \ No newline at end of file diff --git a/ruby/ql/test/library-tests/frameworks/grape/Grape.expected b/ruby/ql/test/library-tests/frameworks/grape/Grape.expected index 904cb36333a4..7e792465911b 100644 --- a/ruby/ql/test/library-tests/frameworks/grape/Grape.expected +++ b/ruby/ql/test/library-tests/frameworks/grape/Grape.expected @@ -22,4 +22,4 @@ grapeHeaders | app.rb:9:18:9:24 | call to headers | | app.rb:46:5:46:11 | call to headers | grapeRequest -| app.rb:25:12:25:18 | call to request | \ No newline at end of file +| app.rb:25:12:25:18 | call to request | diff --git a/ruby/ql/test/query-tests/security/cwe-089/ArelInjection.rb b/ruby/ql/test/query-tests/security/cwe-089/ArelInjection.rb index 30832894b9e4..cf0769c0acd6 100644 --- a/ruby/ql/test/query-tests/security/cwe-089/ArelInjection.rb +++ b/ruby/ql/test/query-tests/security/cwe-089/ArelInjection.rb @@ -15,4 +15,27 @@ class PotatoAPI < Grape::API sql = Arel.sql("SELECT * FROM users WHERE name = #{name}") sql = Arel::Nodes::SqlLiteral.new("SELECT * FROM users WHERE name = #{name}") end -end \ No newline at end of file +end + +class SimpleAPI < Grape::API + get '/test' do + x = params[:name] + Arel.sql("SELECT * FROM users WHERE name = #{x}") + end +end + + # Test helper method pattern in Grape helpers block + class TestAPI < Grape::API + helpers do + def vulnerable_helper(user_id) + # BAD: SQL statement constructed from user input passed as parameter + Arel.sql("SELECT * FROM users WHERE id = #{user_id}") + end + end + + get '/helper_test' do + # This should be detected as SQL injection via helper method + user_id = params[:user_id] + vulnerable_helper(user_id) + end + end \ No newline at end of file diff --git a/ruby/ql/test/query-tests/security/cwe-089/SqlInjection.expected b/ruby/ql/test/query-tests/security/cwe-089/SqlInjection.expected index b8b1350882d8..0b14504058ef 100644 --- a/ruby/ql/test/query-tests/security/cwe-089/SqlInjection.expected +++ b/ruby/ql/test/query-tests/security/cwe-089/SqlInjection.expected @@ -85,6 +85,14 @@ edges | ArelInjection.rb:13:5:13:8 | name | ArelInjection.rb:16:39:16:80 | "SELECT * FROM users WHERE nam..." | provenance | AdditionalTaintStep | | ArelInjection.rb:13:12:13:17 | call to params | ArelInjection.rb:13:12:13:29 | ...[...] | provenance | | | ArelInjection.rb:13:12:13:29 | ...[...] | ArelInjection.rb:13:5:13:8 | name | provenance | | +| ArelInjection.rb:22:5:22:5 | x | ArelInjection.rb:23:14:23:52 | "SELECT * FROM users WHERE nam..." | provenance | AdditionalTaintStep | +| ArelInjection.rb:22:9:22:14 | call to params | ArelInjection.rb:22:9:22:21 | ...[...] | provenance | | +| ArelInjection.rb:22:9:22:21 | ...[...] | ArelInjection.rb:22:5:22:5 | x | provenance | | +| ArelInjection.rb:30:29:30:35 | user_id | ArelInjection.rb:32:18:32:60 | "SELECT * FROM users WHERE id ..." | provenance | AdditionalTaintStep | +| ArelInjection.rb:38:7:38:13 | user_id | ArelInjection.rb:39:25:39:31 | user_id | provenance | | +| ArelInjection.rb:38:17:38:22 | call to params | ArelInjection.rb:38:17:38:32 | ...[...] | provenance | | +| ArelInjection.rb:38:17:38:32 | ...[...] | ArelInjection.rb:38:7:38:13 | user_id | provenance | | +| ArelInjection.rb:39:25:39:31 | user_id | ArelInjection.rb:30:29:30:35 | user_id | provenance | AdditionalTaintStep | | PgInjection.rb:6:5:6:8 | name | PgInjection.rb:13:5:13:8 | qry1 : String | provenance | AdditionalTaintStep | | PgInjection.rb:6:5:6:8 | name | PgInjection.rb:19:5:19:8 | qry2 : String | provenance | AdditionalTaintStep | | PgInjection.rb:6:5:6:8 | name | PgInjection.rb:31:5:31:8 | qry3 : String | provenance | AdditionalTaintStep | @@ -218,6 +226,16 @@ nodes | ArelInjection.rb:13:12:13:29 | ...[...] | semmle.label | ...[...] | | ArelInjection.rb:15:20:15:61 | "SELECT * FROM users WHERE nam..." | semmle.label | "SELECT * FROM users WHERE nam..." | | ArelInjection.rb:16:39:16:80 | "SELECT * FROM users WHERE nam..." | semmle.label | "SELECT * FROM users WHERE nam..." | +| ArelInjection.rb:22:5:22:5 | x | semmle.label | x | +| ArelInjection.rb:22:9:22:14 | call to params | semmle.label | call to params | +| ArelInjection.rb:22:9:22:21 | ...[...] | semmle.label | ...[...] | +| ArelInjection.rb:23:14:23:52 | "SELECT * FROM users WHERE nam..." | semmle.label | "SELECT * FROM users WHERE nam..." | +| ArelInjection.rb:30:29:30:35 | user_id | semmle.label | user_id | +| ArelInjection.rb:32:18:32:60 | "SELECT * FROM users WHERE id ..." | semmle.label | "SELECT * FROM users WHERE id ..." | +| ArelInjection.rb:38:7:38:13 | user_id | semmle.label | user_id | +| ArelInjection.rb:38:17:38:22 | call to params | semmle.label | call to params | +| ArelInjection.rb:38:17:38:32 | ...[...] | semmle.label | ...[...] | +| ArelInjection.rb:39:25:39:31 | user_id | semmle.label | user_id | | PgInjection.rb:6:5:6:8 | name | semmle.label | name | | PgInjection.rb:6:12:6:17 | call to params | semmle.label | call to params | | PgInjection.rb:6:12:6:24 | ...[...] | semmle.label | ...[...] | @@ -277,6 +295,8 @@ subpaths | ArelInjection.rb:7:39:7:80 | "SELECT * FROM users WHERE nam..." | ArelInjection.rb:4:12:4:17 | call to params | ArelInjection.rb:7:39:7:80 | "SELECT * FROM users WHERE nam..." | This SQL query depends on a $@. | ArelInjection.rb:4:12:4:17 | call to params | user-provided value | | ArelInjection.rb:15:20:15:61 | "SELECT * FROM users WHERE nam..." | ArelInjection.rb:13:12:13:17 | call to params | ArelInjection.rb:15:20:15:61 | "SELECT * FROM users WHERE nam..." | This SQL query depends on a $@. | ArelInjection.rb:13:12:13:17 | call to params | user-provided value | | ArelInjection.rb:16:39:16:80 | "SELECT * FROM users WHERE nam..." | ArelInjection.rb:13:12:13:17 | call to params | ArelInjection.rb:16:39:16:80 | "SELECT * FROM users WHERE nam..." | This SQL query depends on a $@. | ArelInjection.rb:13:12:13:17 | call to params | user-provided value | +| ArelInjection.rb:23:14:23:52 | "SELECT * FROM users WHERE nam..." | ArelInjection.rb:22:9:22:14 | call to params | ArelInjection.rb:23:14:23:52 | "SELECT * FROM users WHERE nam..." | This SQL query depends on a $@. | ArelInjection.rb:22:9:22:14 | call to params | user-provided value | +| ArelInjection.rb:32:18:32:60 | "SELECT * FROM users WHERE id ..." | ArelInjection.rb:38:17:38:22 | call to params | ArelInjection.rb:32:18:32:60 | "SELECT * FROM users WHERE id ..." | This SQL query depends on a $@. | ArelInjection.rb:38:17:38:22 | call to params | user-provided value | | PgInjection.rb:14:15:14:18 | qry1 | PgInjection.rb:6:12:6:17 | call to params | PgInjection.rb:14:15:14:18 | qry1 | This SQL query depends on a $@. | PgInjection.rb:6:12:6:17 | call to params | user-provided value | | PgInjection.rb:15:21:15:24 | qry1 | PgInjection.rb:6:12:6:17 | call to params | PgInjection.rb:15:21:15:24 | qry1 | This SQL query depends on a $@. | PgInjection.rb:6:12:6:17 | call to params | user-provided value | | PgInjection.rb:20:22:20:25 | qry2 | PgInjection.rb:6:12:6:17 | call to params | PgInjection.rb:20:22:20:25 | qry2 | This SQL query depends on a $@. | PgInjection.rb:6:12:6:17 | call to params | user-provided value | From 5cfa6e83b390aafe6783eba0e282674a8247db46 Mon Sep 17 00:00:00 2001 From: Chad Bentz <1760475+felickz@users.noreply.github.com> Date: Fri, 12 Sep 2025 22:51:47 -0400 Subject: [PATCH 04/19] Add support for route parameters(+ blocks), headers, and cookies in Grape API --- ruby/ql/lib/codeql/ruby/frameworks/Grape.qll | 92 ++++++++++++++++++- .../frameworks/grape/Grape.expected | 37 +++++--- .../library-tests/frameworks/grape/Grape.ql | 6 +- .../library-tests/frameworks/grape/app.rb | 42 +++++++++ .../security/cwe-089/ArelInjection.rb | 32 ++++++- .../security/cwe-089/SqlInjection.expected | 53 +++++++++-- 6 files changed, 239 insertions(+), 23 deletions(-) diff --git a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll index a3aa2f684c79..fbab28180b8b 100644 --- a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll +++ b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll @@ -137,12 +137,14 @@ private class GrapeParamsCall extends ParamsCallImpl { ) } }/** - * A call to `headers` from within a Grape API endpoint. + * 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" } @@ -179,6 +181,20 @@ class GrapeRequestSource extends Http::Server::RequestInputAccess::Range { 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. */ @@ -194,6 +210,80 @@ private class GrapeRequestCall extends MethodCall { } } +/** + * 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. diff --git a/ruby/ql/test/library-tests/frameworks/grape/Grape.expected b/ruby/ql/test/library-tests/frameworks/grape/Grape.expected index 7e792465911b..c0bee75371c2 100644 --- a/ruby/ql/test/library-tests/frameworks/grape/Grape.expected +++ b/ruby/ql/test/library-tests/frameworks/grape/Grape.expected @@ -1,15 +1,18 @@ grapeAPIClasses -| app.rb:1:1:48:3 | MyAPI | -| app.rb:50:1:54:3 | AdminAPI | +| app.rb:1:1:90:3 | MyAPI | +| app.rb:92:1:96:3 | AdminAPI | grapeEndpoints -| app.rb:1:1:48:3 | MyAPI | app.rb:7:3:11:5 | call to get | GET | /hello/:name | -| app.rb:1:1:48:3 | MyAPI | app.rb:17:3:20:5 | call to post | POST | /messages | -| app.rb:1:1:48:3 | MyAPI | app.rb:23:3:27:5 | call to put | PUT | /update/:id | -| app.rb:1:1:48:3 | MyAPI | app.rb:30:3:32:5 | call to delete | DELETE | /items/:id | -| app.rb:1:1:48:3 | MyAPI | app.rb:35:3:37:5 | call to patch | PATCH | /items/:id | -| app.rb:1:1:48:3 | MyAPI | app.rb:40:3:42:5 | call to head | HEAD | /status | -| app.rb:1:1:48:3 | MyAPI | app.rb:45:3:47:5 | call to options | OPTIONS | /info | -| app.rb:50:1:54:3 | AdminAPI | app.rb:51:3:53:5 | call to get | GET | /admin | +| 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 | grapeParams | app.rb:8:12:8:17 | call to params | | app.rb:14:3:16:5 | call to params | @@ -17,9 +20,21 @@ grapeParams | 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:52:5:52:10 | call to params | +| app.rb:60:12:60:17 | call to params | +| app.rb:94:5:94:10 | 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 | grapeRequest | app.rb:25:12:25:18 | 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 | +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 | diff --git a/ruby/ql/test/library-tests/frameworks/grape/Grape.ql b/ruby/ql/test/library-tests/frameworks/grape/Grape.ql index 3dd7c488a497..63d59d0bdd7d 100644 --- a/ruby/ql/test/library-tests/frameworks/grape/Grape.ql +++ b/ruby/ql/test/library-tests/frameworks/grape/Grape.ql @@ -15,4 +15,8 @@ query predicate grapeParams(GrapeParamsSource params) { any() } query predicate grapeHeaders(GrapeHeadersSource headers) { any() } -query predicate grapeRequest(GrapeRequestSource request) { any() } \ No newline at end of file +query predicate grapeRequest(GrapeRequestSource request) { any() } + +query predicate grapeRouteParam(GrapeRouteParamSource routeParam) { any() } + +query predicate grapeCookies(GrapeCookiesSource cookies) { any() } \ No newline at end of file diff --git a/ruby/ql/test/library-tests/frameworks/grape/app.rb b/ruby/ql/test/library-tests/frameworks/grape/app.rb index 6333240debe8..a034f325f7b3 100644 --- a/ruby/ql/test/library-tests/frameworks/grape/app.rb +++ b/ruby/ql/test/library-tests/frameworks/grape/app.rb @@ -45,6 +45,48 @@ class MyAPI < Grape::API 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 diff --git a/ruby/ql/test/query-tests/security/cwe-089/ArelInjection.rb b/ruby/ql/test/query-tests/security/cwe-089/ArelInjection.rb index cf0769c0acd6..8c9c3bff4fbb 100644 --- a/ruby/ql/test/query-tests/security/cwe-089/ArelInjection.rb +++ b/ruby/ql/test/query-tests/security/cwe-089/ArelInjection.rb @@ -33,9 +33,39 @@ def vulnerable_helper(user_id) 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 + # BAD: Comprehensive test using all Grape input sources in one SQL query + 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 + + # All sources flow to SQL injection + Arel.sql("SELECT * FROM users WHERE id = #{user_id} AND route_id = #{route_id} AND auth = #{auth} AND session = #{session} AND data = #{body_data}") + end + get '/helper_test' do - # This should be detected as SQL injection via helper method + # BAD: Test helper method dataflow user_id = params[:user_id] vulnerable_helper(user_id) end + + # Test route_param block pattern + route_param :id do + get do + # BAD: params[:id] should be user input from the path + user_id = params[:id] + Arel.sql("SELECT * FROM users WHERE id = #{user_id}") + end + end end \ No newline at end of file diff --git a/ruby/ql/test/query-tests/security/cwe-089/SqlInjection.expected b/ruby/ql/test/query-tests/security/cwe-089/SqlInjection.expected index 0b14504058ef..34128474cb93 100644 --- a/ruby/ql/test/query-tests/security/cwe-089/SqlInjection.expected +++ b/ruby/ql/test/query-tests/security/cwe-089/SqlInjection.expected @@ -89,10 +89,24 @@ edges | ArelInjection.rb:22:9:22:14 | call to params | ArelInjection.rb:22:9:22:21 | ...[...] | provenance | | | ArelInjection.rb:22:9:22:21 | ...[...] | ArelInjection.rb:22:5:22:5 | x | provenance | | | ArelInjection.rb:30:29:30:35 | user_id | ArelInjection.rb:32:18:32:60 | "SELECT * FROM users WHERE id ..." | provenance | AdditionalTaintStep | -| ArelInjection.rb:38:7:38:13 | user_id | ArelInjection.rb:39:25:39:31 | user_id | provenance | | -| ArelInjection.rb:38:17:38:22 | call to params | ArelInjection.rb:38:17:38:32 | ...[...] | provenance | | -| ArelInjection.rb:38:17:38:32 | ...[...] | ArelInjection.rb:38:7:38:13 | user_id | provenance | | -| ArelInjection.rb:39:25:39:31 | user_id | ArelInjection.rb:30:29:30:35 | user_id | provenance | AdditionalTaintStep | +| ArelInjection.rb:47:7:47:13 | user_id | ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | provenance | AdditionalTaintStep | +| ArelInjection.rb:47:17:47:22 | call to params | ArelInjection.rb:47:17:47:32 | ...[...] | provenance | | +| ArelInjection.rb:47:17:47:32 | ...[...] | ArelInjection.rb:47:7:47:13 | user_id | provenance | | +| ArelInjection.rb:48:7:48:14 | route_id | ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | provenance | AdditionalTaintStep | +| ArelInjection.rb:48:18:48:38 | call to route_param | ArelInjection.rb:48:7:48:14 | route_id | provenance | | +| ArelInjection.rb:49:7:49:10 | auth | ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | provenance | AdditionalTaintStep | +| ArelInjection.rb:49:14:49:20 | call to headers | ArelInjection.rb:49:14:49:36 | ...[...] | provenance | | +| ArelInjection.rb:49:14:49:36 | ...[...] | ArelInjection.rb:49:7:49:10 | auth | provenance | | +| ArelInjection.rb:50:7:50:13 | session | ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | provenance | AdditionalTaintStep | +| ArelInjection.rb:50:17:50:23 | call to cookies | ArelInjection.rb:50:17:50:36 | ...[...] | provenance | | +| ArelInjection.rb:50:17:50:36 | ...[...] | ArelInjection.rb:50:7:50:13 | session | provenance | | +| ArelInjection.rb:59:7:59:13 | user_id | ArelInjection.rb:60:25:60:31 | user_id | provenance | | +| ArelInjection.rb:59:17:59:22 | call to params | ArelInjection.rb:59:17:59:32 | ...[...] | provenance | | +| ArelInjection.rb:59:17:59:32 | ...[...] | ArelInjection.rb:59:7:59:13 | user_id | provenance | | +| ArelInjection.rb:60:25:60:31 | user_id | ArelInjection.rb:30:29:30:35 | user_id | provenance | AdditionalTaintStep | +| ArelInjection.rb:67:9:67:15 | user_id | ArelInjection.rb:68:18:68:60 | "SELECT * FROM users WHERE id ..." | provenance | AdditionalTaintStep | +| ArelInjection.rb:67:19:67:24 | call to params | ArelInjection.rb:67:19:67:29 | ...[...] | provenance | | +| ArelInjection.rb:67:19:67:29 | ...[...] | ArelInjection.rb:67:9:67:15 | user_id | provenance | | | PgInjection.rb:6:5:6:8 | name | PgInjection.rb:13:5:13:8 | qry1 : String | provenance | AdditionalTaintStep | | PgInjection.rb:6:5:6:8 | name | PgInjection.rb:19:5:19:8 | qry2 : String | provenance | AdditionalTaintStep | | PgInjection.rb:6:5:6:8 | name | PgInjection.rb:31:5:31:8 | qry3 : String | provenance | AdditionalTaintStep | @@ -232,10 +246,26 @@ nodes | ArelInjection.rb:23:14:23:52 | "SELECT * FROM users WHERE nam..." | semmle.label | "SELECT * FROM users WHERE nam..." | | ArelInjection.rb:30:29:30:35 | user_id | semmle.label | user_id | | ArelInjection.rb:32:18:32:60 | "SELECT * FROM users WHERE id ..." | semmle.label | "SELECT * FROM users WHERE id ..." | -| ArelInjection.rb:38:7:38:13 | user_id | semmle.label | user_id | -| ArelInjection.rb:38:17:38:22 | call to params | semmle.label | call to params | -| ArelInjection.rb:38:17:38:32 | ...[...] | semmle.label | ...[...] | -| ArelInjection.rb:39:25:39:31 | user_id | semmle.label | user_id | +| ArelInjection.rb:47:7:47:13 | user_id | semmle.label | user_id | +| ArelInjection.rb:47:17:47:22 | call to params | semmle.label | call to params | +| ArelInjection.rb:47:17:47:32 | ...[...] | semmle.label | ...[...] | +| ArelInjection.rb:48:7:48:14 | route_id | semmle.label | route_id | +| ArelInjection.rb:48:18:48:38 | call to route_param | semmle.label | call to route_param | +| ArelInjection.rb:49:7:49:10 | auth | semmle.label | auth | +| ArelInjection.rb:49:14:49:20 | call to headers | semmle.label | call to headers | +| ArelInjection.rb:49:14:49:36 | ...[...] | semmle.label | ...[...] | +| ArelInjection.rb:50:7:50:13 | session | semmle.label | session | +| ArelInjection.rb:50:17:50:23 | call to cookies | semmle.label | call to cookies | +| ArelInjection.rb:50:17:50:36 | ...[...] | semmle.label | ...[...] | +| ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | semmle.label | "SELECT * FROM users WHERE id ..." | +| ArelInjection.rb:59:7:59:13 | user_id | semmle.label | user_id | +| ArelInjection.rb:59:17:59:22 | call to params | semmle.label | call to params | +| ArelInjection.rb:59:17:59:32 | ...[...] | semmle.label | ...[...] | +| ArelInjection.rb:60:25:60:31 | user_id | semmle.label | user_id | +| ArelInjection.rb:67:9:67:15 | user_id | semmle.label | user_id | +| ArelInjection.rb:67:19:67:24 | call to params | semmle.label | call to params | +| ArelInjection.rb:67:19:67:29 | ...[...] | semmle.label | ...[...] | +| ArelInjection.rb:68:18:68:60 | "SELECT * FROM users WHERE id ..." | semmle.label | "SELECT * FROM users WHERE id ..." | | PgInjection.rb:6:5:6:8 | name | semmle.label | name | | PgInjection.rb:6:12:6:17 | call to params | semmle.label | call to params | | PgInjection.rb:6:12:6:24 | ...[...] | semmle.label | ...[...] | @@ -296,7 +326,12 @@ subpaths | ArelInjection.rb:15:20:15:61 | "SELECT * FROM users WHERE nam..." | ArelInjection.rb:13:12:13:17 | call to params | ArelInjection.rb:15:20:15:61 | "SELECT * FROM users WHERE nam..." | This SQL query depends on a $@. | ArelInjection.rb:13:12:13:17 | call to params | user-provided value | | ArelInjection.rb:16:39:16:80 | "SELECT * FROM users WHERE nam..." | ArelInjection.rb:13:12:13:17 | call to params | ArelInjection.rb:16:39:16:80 | "SELECT * FROM users WHERE nam..." | This SQL query depends on a $@. | ArelInjection.rb:13:12:13:17 | call to params | user-provided value | | ArelInjection.rb:23:14:23:52 | "SELECT * FROM users WHERE nam..." | ArelInjection.rb:22:9:22:14 | call to params | ArelInjection.rb:23:14:23:52 | "SELECT * FROM users WHERE nam..." | This SQL query depends on a $@. | ArelInjection.rb:22:9:22:14 | call to params | user-provided value | -| ArelInjection.rb:32:18:32:60 | "SELECT * FROM users WHERE id ..." | ArelInjection.rb:38:17:38:22 | call to params | ArelInjection.rb:32:18:32:60 | "SELECT * FROM users WHERE id ..." | This SQL query depends on a $@. | ArelInjection.rb:38:17:38:22 | call to params | user-provided value | +| ArelInjection.rb:32:18:32:60 | "SELECT * FROM users WHERE id ..." | ArelInjection.rb:59:17:59:22 | call to params | ArelInjection.rb:32:18:32:60 | "SELECT * FROM users WHERE id ..." | This SQL query depends on a $@. | ArelInjection.rb:59:17:59:22 | call to params | user-provided value | +| ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | ArelInjection.rb:47:17:47:22 | call to params | ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | This SQL query depends on a $@. | ArelInjection.rb:47:17:47:22 | call to params | user-provided value | +| ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | ArelInjection.rb:48:18:48:38 | call to route_param | ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | This SQL query depends on a $@. | ArelInjection.rb:48:18:48:38 | call to route_param | user-provided value | +| ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | ArelInjection.rb:49:14:49:20 | call to headers | ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | This SQL query depends on a $@. | ArelInjection.rb:49:14:49:20 | call to headers | user-provided value | +| ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | ArelInjection.rb:50:17:50:23 | call to cookies | ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | This SQL query depends on a $@. | ArelInjection.rb:50:17:50:23 | call to cookies | user-provided value | +| ArelInjection.rb:68:18:68:60 | "SELECT * FROM users WHERE id ..." | ArelInjection.rb:67:19:67:24 | call to params | ArelInjection.rb:68:18:68:60 | "SELECT * FROM users WHERE id ..." | This SQL query depends on a $@. | ArelInjection.rb:67:19:67:24 | call to params | user-provided value | | PgInjection.rb:14:15:14:18 | qry1 | PgInjection.rb:6:12:6:17 | call to params | PgInjection.rb:14:15:14:18 | qry1 | This SQL query depends on a $@. | PgInjection.rb:6:12:6:17 | call to params | user-provided value | | PgInjection.rb:15:21:15:24 | qry1 | PgInjection.rb:6:12:6:17 | call to params | PgInjection.rb:15:21:15:24 | qry1 | This SQL query depends on a $@. | PgInjection.rb:6:12:6:17 | call to params | user-provided value | | PgInjection.rb:20:22:20:25 | qry2 | PgInjection.rb:6:12:6:17 | call to params | PgInjection.rb:20:22:20:25 | qry2 | This SQL query depends on a $@. | PgInjection.rb:6:12:6:17 | call to params | user-provided value | From a8d4d6b5630f7bd4804c51aefd8b0f84ef00ffe1 Mon Sep 17 00:00:00 2001 From: Chad Bentz <1760475+felickz@users.noreply.github.com> Date: Mon, 15 Sep 2025 22:02:03 -0400 Subject: [PATCH 05/19] Apply naming standards + changenote --- .../2025-09-15-grape-framework-support.md | 4 ++ ruby/ql/lib/codeql/ruby/frameworks/Grape.qll | 44 +++++++++---------- .../frameworks/grape/Grape.expected | 2 +- .../library-tests/frameworks/grape/Grape.ql | 4 +- 4 files changed, 29 insertions(+), 25 deletions(-) create mode 100644 ruby/ql/lib/change-notes/2025-09-15-grape-framework-support.md diff --git a/ruby/ql/lib/change-notes/2025-09-15-grape-framework-support.md b/ruby/ql/lib/change-notes/2025-09-15-grape-framework-support.md new file mode 100644 index 000000000000..258da40d36c5 --- /dev/null +++ b/ruby/ql/lib/change-notes/2025-09-15-grape-framework-support.md @@ -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. \ No newline at end of file diff --git a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll index fbab28180b8b..72dd1e13b9bc 100644 --- a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll +++ b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll @@ -23,9 +23,9 @@ 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()) + class RootApi extends GrapeApiClass { + RootApi() { + not exists(GrapeApiClass parent | this != parent and this = parent.getADescendent()) } } } @@ -43,17 +43,17 @@ module Grape { * end * ``` */ -class GrapeAPIClass extends DataFlow::ClassNode { - GrapeAPIClass() { - this = grapeAPIBaseClass().getADescendentModule() and - not exists(DataFlow::ModuleNode m | m = grapeAPIBaseClass().asModule() | this = m) +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 + result.getApiClass() = this } /** @@ -68,19 +68,19 @@ class GrapeAPIClass extends DataFlow::ClassNode { } } -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 { - private GrapeAPIClass apiClass; + private GrapeApiClass apiClass; GrapeEndpoint() { this = apiClass.getAModuleLevelCall(["get", "post", "put", "delete", "patch", "head", "options"]) @@ -96,7 +96,7 @@ class GrapeEndpoint extends DataFlow::CallNode { /** * Gets the API class containing this endpoint. */ - GrapeAPIClass getAPIClass() { result = apiClass } + GrapeApiClass getApiClass() { result = apiClass } /** * Gets the block containing the endpoint logic. @@ -131,7 +131,7 @@ class GrapeParamsSource extends Http::Server::RequestInputAccess::Range { private class GrapeParamsCall extends ParamsCallImpl { GrapeParamsCall() { // Simplified approach: find params calls that are descendants of Grape API class methods - exists(GrapeAPIClass api | + exists(GrapeApiClass api | this.getMethodName() = "params" and this.getParent+() = api.getADeclaration() ) @@ -163,7 +163,7 @@ private class GrapeHeadersCall extends MethodCall { ) or // Also handle cases where headers is called on an instance of a Grape API class - this = grapeAPIInstance().getAMethodCall("headers").asExpr().getExpr() + this = grapeApiInstance().getAMethodCall("headers").asExpr().getExpr() } } @@ -206,7 +206,7 @@ private class GrapeRequestCall extends MethodCall { ) or // Also handle cases where request is called on an instance of a Grape API class - this = grapeAPIInstance().getAMethodCall("request").asExpr().getExpr() + this = grapeApiInstance().getAMethodCall("request").asExpr().getExpr() } } @@ -221,7 +221,7 @@ private class GrapeRouteParamCall extends MethodCall { ) or // Also handle cases where route_param is called on an instance of a Grape API class - this = grapeAPIInstance().getAMethodCall("route_param").asExpr().getExpr() + this = grapeApiInstance().getAMethodCall("route_param").asExpr().getExpr() } } @@ -231,7 +231,7 @@ private class GrapeRouteParamCall extends MethodCall { */ private class GrapeHeadersBlockCall extends MethodCall { GrapeHeadersBlockCall() { - exists(GrapeAPIClass api | + exists(GrapeApiClass api | this.getParent+() = api.getADeclaration() and this.getMethodName() = "headers" and exists(this.getBlock()) @@ -245,7 +245,7 @@ private class GrapeHeadersBlockCall extends MethodCall { */ private class GrapeCookiesBlockCall extends MethodCall { GrapeCookiesBlockCall() { - exists(GrapeAPIClass api | + exists(GrapeApiClass api | this.getParent+() = api.getADeclaration() and this.getMethodName() = "cookies" and exists(this.getBlock()) @@ -280,7 +280,7 @@ private class GrapeCookiesCall extends MethodCall { ) or // Also handle cases where cookies is called on an instance of a Grape API class - this = grapeAPIInstance().getAMethodCall("cookies").asExpr().getExpr() + this = grapeApiInstance().getAMethodCall("cookies").asExpr().getExpr() } } @@ -289,7 +289,7 @@ private class GrapeCookiesCall extends MethodCall { * These methods become available in endpoint contexts through Grape's DSL. */ private class GrapeHelperMethod extends Method { - private GrapeAPIClass apiClass; + private GrapeApiClass apiClass; GrapeHelperMethod() { exists(DataFlow::CallNode helpersCall | @@ -301,7 +301,7 @@ private class GrapeHelperMethod extends Method { /** * Gets the API class that contains this helper method. */ - GrapeAPIClass getAPIClass() { result = apiClass } + GrapeApiClass getAPIClass() { result = apiClass } } /** diff --git a/ruby/ql/test/library-tests/frameworks/grape/Grape.expected b/ruby/ql/test/library-tests/frameworks/grape/Grape.expected index c0bee75371c2..af4d936e88d1 100644 --- a/ruby/ql/test/library-tests/frameworks/grape/Grape.expected +++ b/ruby/ql/test/library-tests/frameworks/grape/Grape.expected @@ -1,4 +1,4 @@ -grapeAPIClasses +grapeApiClasses | app.rb:1:1:90:3 | MyAPI | | app.rb:92:1:96:3 | AdminAPI | grapeEndpoints diff --git a/ruby/ql/test/library-tests/frameworks/grape/Grape.ql b/ruby/ql/test/library-tests/frameworks/grape/Grape.ql index 63d59d0bdd7d..ebfb304dbe7a 100644 --- a/ruby/ql/test/library-tests/frameworks/grape/Grape.ql +++ b/ruby/ql/test/library-tests/frameworks/grape/Grape.ql @@ -3,9 +3,9 @@ import codeql.ruby.frameworks.Grape import codeql.ruby.Concepts import codeql.ruby.AST -query predicate grapeAPIClasses(GrapeAPIClass api) { any() } +query predicate grapeApiClasses(GrapeApiClass api) { any() } -query predicate grapeEndpoints(GrapeAPIClass api, GrapeEndpoint endpoint, string method, string path) { +query predicate grapeEndpoints(GrapeApiClass api, GrapeEndpoint endpoint, string method, string path) { endpoint = api.getAnEndpoint() and method = endpoint.getHttpMethod() and path = endpoint.getPath() From 19cb1874368723b7441b01bb07d6aaf16f8c009d Mon Sep 17 00:00:00 2001 From: Chad Bentz <1760475+felickz@users.noreply.github.com> Date: Mon, 15 Sep 2025 22:03:27 -0400 Subject: [PATCH 06/19] Update ruby/ql/lib/codeql/ruby/frameworks/Grape.qll Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- ruby/ql/lib/codeql/ruby/frameworks/Grape.qll | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll index 72dd1e13b9bc..417d4ee4da4a 100644 --- a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll +++ b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll @@ -136,7 +136,9 @@ private class GrapeParamsCall extends ParamsCallImpl { this.getParent+() = api.getADeclaration() ) } -}/** +} + +/** * A call to `headers` from within a Grape API endpoint or headers block. * Headers can also be a source of user input. */ From fc98cd8d08e9f1d258611757094f73b00095963e Mon Sep 17 00:00:00 2001 From: Chad Bentz <1760475+felickz@users.noreply.github.com> Date: Mon, 15 Sep 2025 22:11:33 -0400 Subject: [PATCH 07/19] Fix naming standards --- ruby/ql/lib/codeql/ruby/frameworks/Grape.qll | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll index 72dd1e13b9bc..7b963c92ee13 100644 --- a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll +++ b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll @@ -301,7 +301,7 @@ private class GrapeHelperMethod extends Method { /** * Gets the API class that contains this helper method. */ - GrapeApiClass getAPIClass() { result = apiClass } + GrapeApiClass getApiClass() { result = apiClass } } /** From ffd32efba274f0b1400d592562f24cae4a286ddf Mon Sep 17 00:00:00 2001 From: Chad Bentz <1760475+felickz@users.noreply.github.com> Date: Tue, 16 Sep 2025 09:08:07 -0400 Subject: [PATCH 08/19] codeql query format --- ruby/ql/lib/codeql/ruby/frameworks/Grape.qll | 37 ++++++------------- .../library-tests/frameworks/grape/Grape.ql | 2 +- 2 files changed, 12 insertions(+), 27 deletions(-) diff --git a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll index faf762f53a03..ea7bc8c576c4 100644 --- a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll +++ b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll @@ -52,9 +52,7 @@ class GrapeApiClass extends DataFlow::ClassNode { /** * Gets a `GrapeEndpoint` defined in this class. */ - GrapeEndpoint getAnEndpoint() { - result.getApiClass() = this - } + GrapeEndpoint getAnEndpoint() { result.getApiClass() = this } /** * Gets a `self` that possibly refers to an instance of this class. @@ -72,9 +70,7 @@ 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. @@ -83,15 +79,14 @@ class GrapeEndpoint extends DataFlow::CallNode { private GrapeApiClass apiClass; GrapeEndpoint() { - this = apiClass.getAModuleLevelCall(["get", "post", "put", "delete", "patch", "head", "options"]) + 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() - } + string getHttpMethod() { result = this.getMethodName().toUpperCase() } /** * Gets the API class containing this endpoint. @@ -106,9 +101,7 @@ class GrapeEndpoint extends DataFlow::CallNode { /** * Gets the path pattern for this endpoint, if specified. */ - string getPath() { - result = this.getArgument(0).getConstantValue().getString() - } + string getPath() { result = this.getArgument(0).getConstantValue().getString() } } /** @@ -116,9 +109,7 @@ class GrapeEndpoint extends DataFlow::CallNode { * Grape parameters available via the `params` method within an endpoint. */ class GrapeParamsSource extends Http::Server::RequestInputAccess::Range { - GrapeParamsSource() { - this.asExpr().getExpr() instanceof GrapeParamsCall - } + GrapeParamsSource() { this.asExpr().getExpr() instanceof GrapeParamsCall } override string getSourceType() { result = "Grape::API#params" } @@ -174,9 +165,7 @@ private class GrapeHeadersCall extends MethodCall { * The request object can contain user input. */ class GrapeRequestSource extends Http::Server::RequestInputAccess::Range { - GrapeRequestSource() { - this.asExpr().getExpr() instanceof GrapeRequestCall - } + GrapeRequestSource() { this.asExpr().getExpr() instanceof GrapeRequestCall } override string getSourceType() { result = "Grape::API#request" } @@ -188,9 +177,7 @@ class GrapeRequestSource extends Http::Server::RequestInputAccess::Range { * 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 - } + GrapeRouteParamSource() { this.asExpr().getExpr() instanceof GrapeRouteParamCall } override string getSourceType() { result = "Grape::API#route_param" } @@ -316,12 +303,10 @@ private class GrapeHelperMethodTaintStep extends AdditionalTaintStep { exists(GrapeHelperMethod helperMethod, MethodCall call, int i | // Find calls to helper methods from within Grape endpoints call.getMethodName() = helperMethod.getName() and - exists(GrapeEndpoint endpoint | - call.getParent+() = endpoint.getBody().asExpr().getExpr() - ) and + exists(GrapeEndpoint endpoint | call.getParent+() = endpoint.getBody().asExpr().getExpr()) and // Map argument to parameter nodeFrom.asExpr().getExpr() = call.getArgument(i) and nodeTo.asParameter() = helperMethod.getParameter(i) ) } -} \ No newline at end of file +} diff --git a/ruby/ql/test/library-tests/frameworks/grape/Grape.ql b/ruby/ql/test/library-tests/frameworks/grape/Grape.ql index ebfb304dbe7a..c9aa7c29082c 100644 --- a/ruby/ql/test/library-tests/frameworks/grape/Grape.ql +++ b/ruby/ql/test/library-tests/frameworks/grape/Grape.ql @@ -19,4 +19,4 @@ query predicate grapeRequest(GrapeRequestSource request) { any() } query predicate grapeRouteParam(GrapeRouteParamSource routeParam) { any() } -query predicate grapeCookies(GrapeCookiesSource cookies) { any() } \ No newline at end of file +query predicate grapeCookies(GrapeCookiesSource cookies) { any() } From c5e3be2c4cc30b1d8b92dfde67097577d4867dc0 Mon Sep 17 00:00:00 2001 From: Chad Bentz <1760475+felickz@users.noreply.github.com> Date: Tue, 16 Sep 2025 17:09:18 -0400 Subject: [PATCH 09/19] Grape - detect params calls inside helper methods - added unit tests for flow using inline format - removed grape from Arel tests (temporary) --- ruby/ql/lib/codeql/ruby/frameworks/Grape.qll | 28 ++++++- .../frameworks/grape/Flow.expected | 77 +++++++++++++++++++ .../library-tests/frameworks/grape/Flow.ql | 25 ++++++ .../frameworks/grape/Grape.expected | 16 ++++ .../library-tests/frameworks/grape/app.rb | 74 +++++++++++++++++- .../security/cwe-089/ArelInjection.rb | 64 +-------------- .../security/cwe-089/SqlInjection.expected | 66 ---------------- 7 files changed, 216 insertions(+), 134 deletions(-) create mode 100644 ruby/ql/test/library-tests/frameworks/grape/Flow.expected create mode 100644 ruby/ql/test/library-tests/frameworks/grape/Flow.ql diff --git a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll index ea7bc8c576c4..a1646b8654c9 100644 --- a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll +++ b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll @@ -121,11 +121,18 @@ class GrapeParamsSource extends Http::Server::RequestInputAccess::Range { */ private class GrapeParamsCall extends ParamsCallImpl { GrapeParamsCall() { - // Simplified approach: find params calls that are descendants of Grape API class methods + // 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() + ) } } @@ -295,18 +302,31 @@ private class GrapeHelperMethod extends Method { /** * Additional taint step to model dataflow from method arguments to parameters - * for Grape helper methods defined in `helpers` blocks. + * 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 + // Find calls to helper methods from within Grape endpoints or other helper methods call.getMethodName() = helperMethod.getName() and - exists(GrapeEndpoint endpoint | call.getParent+() = endpoint.getBody().asExpr().getExpr()) 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 + ) } } diff --git a/ruby/ql/test/library-tests/frameworks/grape/Flow.expected b/ruby/ql/test/library-tests/frameworks/grape/Flow.expected new file mode 100644 index 000000000000..0fd19d4eaced --- /dev/null +++ b/ruby/ql/test/library-tests/frameworks/grape/Flow.expected @@ -0,0 +1,77 @@ +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:70 | call to select | app.rb:149:21:149:31 | call to user_params | provenance | AdditionalTaintStep | +| app.rb:103:13:103:70 | call to select | app.rb:165:21:165:31 | call to user_params | provenance | AdditionalTaintStep | +| app.rb:107:13:107:32 | call to source | app.rb:143:18:143:43 | call to vulnerable_helper | provenance | AdditionalTaintStep | +| app.rb:111:13:111:33 | call to source | app.rb:150:25:150:37 | call to simple_helper | provenance | AdditionalTaintStep | +| app.rb:126:9:126:15 | user_id | app.rb:133:14:133:20 | user_id | provenance | | +| app.rb:126:19:126:24 | call to params | app.rb:126:19:126:34 | ...[...] | provenance | | +| app.rb:126:19:126:34 | ...[...] | app.rb:126:9:126:15 | user_id | provenance | | +| app.rb:127:9:127:16 | route_id | app.rb:134:14:134:21 | route_id | provenance | | +| app.rb:127:20:127:40 | call to route_param | app.rb:127:9:127:16 | route_id | provenance | | +| app.rb:128:9:128:12 | auth | app.rb:135:14:135:17 | auth | provenance | | +| app.rb:128:16:128:22 | call to headers | app.rb:128:16:128:38 | ...[...] | provenance | | +| app.rb:128:16:128:38 | ...[...] | app.rb:128:9:128:12 | auth | provenance | | +| app.rb:129:9:129:15 | session | app.rb:136:14:136:20 | session | provenance | | +| app.rb:129:19:129:25 | call to cookies | app.rb:129:19:129:38 | ...[...] | provenance | | +| app.rb:129:19:129:38 | ...[...] | app.rb:129:9:129:15 | session | provenance | | +| app.rb:143:9:143:14 | result | app.rb:144:14:144:19 | result | provenance | | +| app.rb:143:18:143:43 | call to vulnerable_helper | app.rb:143:9:143:14 | result | provenance | | +| app.rb:149:9:149:17 | user_data | app.rb:151:14:151:22 | user_data | provenance | | +| app.rb:149:21:149:31 | call to user_params | app.rb:149:9:149:17 | user_data | provenance | | +| app.rb:150:9:150:21 | simple_result | app.rb:152:14:152:26 | simple_result | provenance | | +| app.rb:150:25:150:37 | call to simple_helper | app.rb:150:9:150:21 | simple_result | provenance | | +| app.rb:159:13:159:19 | user_id | app.rb:160:18:160:24 | user_id | provenance | | +| app.rb:159:23:159:28 | call to params | app.rb:159:23:159:33 | ...[...] | provenance | | +| app.rb:159:23:159:33 | ...[...] | app.rb:159:13:159:19 | user_id | provenance | | +| app.rb:165:9:165:17 | user_data | app.rb:166:14:166:22 | user_data | provenance | | +| app.rb:165:21:165:31 | call to user_params | app.rb:165:9:165:17 | user_data | 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: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:126:9:126:15 | user_id | semmle.label | user_id | +| app.rb:126:19:126:24 | call to params | semmle.label | call to params | +| app.rb:126:19:126:34 | ...[...] | semmle.label | ...[...] | +| app.rb:127:9:127:16 | route_id | semmle.label | route_id | +| app.rb:127:20:127:40 | call to route_param | semmle.label | call to route_param | +| app.rb:128:9:128:12 | auth | semmle.label | auth | +| app.rb:128:16:128:22 | call to headers | semmle.label | call to headers | +| app.rb:128:16:128:38 | ...[...] | semmle.label | ...[...] | +| app.rb:129:9:129:15 | session | semmle.label | session | +| app.rb:129:19:129:25 | call to cookies | semmle.label | call to cookies | +| app.rb:129:19:129:38 | ...[...] | semmle.label | ...[...] | +| app.rb:133:14:133:20 | user_id | semmle.label | user_id | +| app.rb:134:14:134:21 | route_id | semmle.label | route_id | +| app.rb:135:14:135:17 | auth | semmle.label | auth | +| app.rb:136:14:136:20 | session | semmle.label | session | +| app.rb:143:9:143:14 | result | semmle.label | result | +| app.rb:143:18:143:43 | call to vulnerable_helper | semmle.label | call to vulnerable_helper | +| app.rb:144:14:144:19 | result | semmle.label | result | +| app.rb:149:9:149:17 | user_data | semmle.label | user_data | +| app.rb:149:21:149:31 | call to user_params | semmle.label | call to user_params | +| app.rb:150:9:150:21 | simple_result | semmle.label | simple_result | +| app.rb:150:25:150:37 | call to simple_helper | semmle.label | call to simple_helper | +| app.rb:151:14:151:22 | user_data | semmle.label | user_data | +| app.rb:152:14:152:26 | simple_result | semmle.label | simple_result | +| app.rb:159:13:159:19 | user_id | semmle.label | user_id | +| app.rb:159:23:159:28 | call to params | semmle.label | call to params | +| app.rb:159:23:159:33 | ...[...] | semmle.label | ...[...] | +| app.rb:160:18:160:24 | user_id | semmle.label | user_id | +| app.rb:165:9:165:17 | user_data | semmle.label | user_data | +| app.rb:165:21:165:31 | call to user_params | semmle.label | call to user_params | +| app.rb:166:14:166:22 | user_data | semmle.label | user_data | +subpaths +testFailures +#select +| app.rb:133:14:133:20 | user_id | app.rb:126:19:126:24 | call to params | app.rb:133:14:133:20 | user_id | $@ | app.rb:126:19:126:24 | call to params | call to params | +| app.rb:134:14:134:21 | route_id | app.rb:127:20:127:40 | call to route_param | app.rb:134:14:134:21 | route_id | $@ | app.rb:127:20:127:40 | call to route_param | call to route_param | +| app.rb:135:14:135:17 | auth | app.rb:128:16:128:22 | call to headers | app.rb:135:14:135:17 | auth | $@ | app.rb:128:16:128:22 | call to headers | call to headers | +| app.rb:136:14:136:20 | session | app.rb:129:19:129:25 | call to cookies | app.rb:136:14:136:20 | session | $@ | app.rb:129:19:129:25 | call to cookies | call to cookies | +| app.rb:144:14:144:19 | result | app.rb:107:13:107:32 | call to source | app.rb:144:14:144:19 | result | $@ | app.rb:107:13:107:32 | call to source | call to source | +| app.rb:151:14:151:22 | user_data | app.rb:103:13:103:18 | call to params | app.rb:151:14:151:22 | user_data | $@ | app.rb:103:13:103:18 | call to params | call to params | +| app.rb:152:14:152:26 | simple_result | app.rb:111:13:111:33 | call to source | app.rb:152:14:152:26 | simple_result | $@ | app.rb:111:13:111:33 | call to source | call to source | +| app.rb:160:18:160:24 | user_id | app.rb:159:23:159:28 | call to params | app.rb:160:18:160:24 | user_id | $@ | app.rb:159:23:159:28 | call to params | call to params | +| app.rb:166:14:166:22 | user_data | app.rb:103:13:103:18 | call to params | app.rb:166:14:166:22 | user_data | $@ | app.rb:103:13:103:18 | call to params | call to params | diff --git a/ruby/ql/test/library-tests/frameworks/grape/Flow.ql b/ruby/ql/test/library-tests/frameworks/grape/Flow.ql new file mode 100644 index 000000000000..baa3fa4307fb --- /dev/null +++ b/ruby/ql/test/library-tests/frameworks/grape/Flow.ql @@ -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 + +from PathNode source, PathNode sink +where flowPath(source, sink) +select sink, source, sink, "$@", source, source.toString() diff --git a/ruby/ql/test/library-tests/frameworks/grape/Grape.expected b/ruby/ql/test/library-tests/frameworks/grape/Grape.expected index af4d936e88d1..d39d9430f926 100644 --- a/ruby/ql/test/library-tests/frameworks/grape/Grape.expected +++ b/ruby/ql/test/library-tests/frameworks/grape/Grape.expected @@ -1,6 +1,7 @@ grapeApiClasses | app.rb:1:1:90:3 | MyAPI | | app.rb:92:1:96:3 | AdminAPI | +| app.rb:98:1:168: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 | @@ -13,6 +14,10 @@ grapeEndpoints | 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:168:3 | UserAPI | app.rb:124:5:138:7 | call to get | GET | /comprehensive_test/:user_id | +| app.rb:98:1:168:3 | UserAPI | app.rb:140:5:145:7 | call to get | GET | /helper_test/:user_id | +| app.rb:98:1:168:3 | UserAPI | app.rb:147:5:153:7 | call to post | POST | /users | +| app.rb:98:1:168:3 | UserAPI | app.rb:164:5:167:7 | call to post | POST | /users | grapeParams | app.rb:8:12:8:17 | call to params | | app.rb:14:3:16:5 | call to params | @@ -22,19 +27,30 @@ grapeParams | 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:126:19:126:24 | call to params | +| app.rb:142:19:142:24 | call to params | +| app.rb:159:23:159: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:116:5:118:7 | call to headers | +| app.rb:128:16:128:22 | call to headers | grapeRequest | app.rb:25:12:25:18 | call to request | +| app.rb:130:21:130: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:127:20:127:40 | call to route_param | +| app.rb:156:5:162: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:120:5:122:7 | call to cookies | +| app.rb:129:19:129:25 | call to cookies | diff --git a/ruby/ql/test/library-tests/frameworks/grape/app.rb b/ruby/ql/test/library-tests/frameworks/grape/app.rb index a034f325f7b3..6fbb184cab98 100644 --- a/ruby/ql/test/library-tests/frameworks/grape/app.rb +++ b/ruby/ql/test/library-tests/frameworks/grape/app.rb @@ -93,4 +93,76 @@ class AdminAPI < Grape::API get '/admin' do params[:token] end -end \ No newline at end of file +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 + 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 # $ hasTaintFlow=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 # $ hasTaintFlow=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 +end diff --git a/ruby/ql/test/query-tests/security/cwe-089/ArelInjection.rb b/ruby/ql/test/query-tests/security/cwe-089/ArelInjection.rb index 8c9c3bff4fbb..1cd6782b2416 100644 --- a/ruby/ql/test/query-tests/security/cwe-089/ArelInjection.rb +++ b/ruby/ql/test/query-tests/security/cwe-089/ArelInjection.rb @@ -6,66 +6,4 @@ def unsafe_action sql = Arel.sql("SELECT * FROM users WHERE name = #{name}") sql = Arel::Nodes::SqlLiteral.new("SELECT * FROM users WHERE name = #{name}") end -end - -class PotatoAPI < Grape::API - get '/unsafe_endpoint' do - name = params[:user_name] - # BAD: SQL statement constructed from user input - sql = Arel.sql("SELECT * FROM users WHERE name = #{name}") - sql = Arel::Nodes::SqlLiteral.new("SELECT * FROM users WHERE name = #{name}") - end -end - -class SimpleAPI < Grape::API - get '/test' do - x = params[:name] - Arel.sql("SELECT * FROM users WHERE name = #{x}") - end -end - - # Test helper method pattern in Grape helpers block - class TestAPI < Grape::API - helpers do - def vulnerable_helper(user_id) - # BAD: SQL statement constructed from user input passed as parameter - Arel.sql("SELECT * FROM users WHERE id = #{user_id}") - 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 - # BAD: Comprehensive test using all Grape input sources in one SQL query - 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 - - # All sources flow to SQL injection - Arel.sql("SELECT * FROM users WHERE id = #{user_id} AND route_id = #{route_id} AND auth = #{auth} AND session = #{session} AND data = #{body_data}") - end - - get '/helper_test' do - # BAD: Test helper method dataflow - user_id = params[:user_id] - vulnerable_helper(user_id) - end - - # Test route_param block pattern - route_param :id do - get do - # BAD: params[:id] should be user input from the path - user_id = params[:id] - Arel.sql("SELECT * FROM users WHERE id = #{user_id}") - end - end - end \ No newline at end of file +end \ No newline at end of file diff --git a/ruby/ql/test/query-tests/security/cwe-089/SqlInjection.expected b/ruby/ql/test/query-tests/security/cwe-089/SqlInjection.expected index 34128474cb93..069cb34810fc 100644 --- a/ruby/ql/test/query-tests/security/cwe-089/SqlInjection.expected +++ b/ruby/ql/test/query-tests/security/cwe-089/SqlInjection.expected @@ -81,32 +81,6 @@ edges | ArelInjection.rb:4:5:4:8 | name | ArelInjection.rb:7:39:7:80 | "SELECT * FROM users WHERE nam..." | provenance | AdditionalTaintStep | | ArelInjection.rb:4:12:4:17 | call to params | ArelInjection.rb:4:12:4:29 | ...[...] | provenance | | | ArelInjection.rb:4:12:4:29 | ...[...] | ArelInjection.rb:4:5:4:8 | name | provenance | | -| ArelInjection.rb:13:5:13:8 | name | ArelInjection.rb:15:20:15:61 | "SELECT * FROM users WHERE nam..." | provenance | AdditionalTaintStep | -| ArelInjection.rb:13:5:13:8 | name | ArelInjection.rb:16:39:16:80 | "SELECT * FROM users WHERE nam..." | provenance | AdditionalTaintStep | -| ArelInjection.rb:13:12:13:17 | call to params | ArelInjection.rb:13:12:13:29 | ...[...] | provenance | | -| ArelInjection.rb:13:12:13:29 | ...[...] | ArelInjection.rb:13:5:13:8 | name | provenance | | -| ArelInjection.rb:22:5:22:5 | x | ArelInjection.rb:23:14:23:52 | "SELECT * FROM users WHERE nam..." | provenance | AdditionalTaintStep | -| ArelInjection.rb:22:9:22:14 | call to params | ArelInjection.rb:22:9:22:21 | ...[...] | provenance | | -| ArelInjection.rb:22:9:22:21 | ...[...] | ArelInjection.rb:22:5:22:5 | x | provenance | | -| ArelInjection.rb:30:29:30:35 | user_id | ArelInjection.rb:32:18:32:60 | "SELECT * FROM users WHERE id ..." | provenance | AdditionalTaintStep | -| ArelInjection.rb:47:7:47:13 | user_id | ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | provenance | AdditionalTaintStep | -| ArelInjection.rb:47:17:47:22 | call to params | ArelInjection.rb:47:17:47:32 | ...[...] | provenance | | -| ArelInjection.rb:47:17:47:32 | ...[...] | ArelInjection.rb:47:7:47:13 | user_id | provenance | | -| ArelInjection.rb:48:7:48:14 | route_id | ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | provenance | AdditionalTaintStep | -| ArelInjection.rb:48:18:48:38 | call to route_param | ArelInjection.rb:48:7:48:14 | route_id | provenance | | -| ArelInjection.rb:49:7:49:10 | auth | ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | provenance | AdditionalTaintStep | -| ArelInjection.rb:49:14:49:20 | call to headers | ArelInjection.rb:49:14:49:36 | ...[...] | provenance | | -| ArelInjection.rb:49:14:49:36 | ...[...] | ArelInjection.rb:49:7:49:10 | auth | provenance | | -| ArelInjection.rb:50:7:50:13 | session | ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | provenance | AdditionalTaintStep | -| ArelInjection.rb:50:17:50:23 | call to cookies | ArelInjection.rb:50:17:50:36 | ...[...] | provenance | | -| ArelInjection.rb:50:17:50:36 | ...[...] | ArelInjection.rb:50:7:50:13 | session | provenance | | -| ArelInjection.rb:59:7:59:13 | user_id | ArelInjection.rb:60:25:60:31 | user_id | provenance | | -| ArelInjection.rb:59:17:59:22 | call to params | ArelInjection.rb:59:17:59:32 | ...[...] | provenance | | -| ArelInjection.rb:59:17:59:32 | ...[...] | ArelInjection.rb:59:7:59:13 | user_id | provenance | | -| ArelInjection.rb:60:25:60:31 | user_id | ArelInjection.rb:30:29:30:35 | user_id | provenance | AdditionalTaintStep | -| ArelInjection.rb:67:9:67:15 | user_id | ArelInjection.rb:68:18:68:60 | "SELECT * FROM users WHERE id ..." | provenance | AdditionalTaintStep | -| ArelInjection.rb:67:19:67:24 | call to params | ArelInjection.rb:67:19:67:29 | ...[...] | provenance | | -| ArelInjection.rb:67:19:67:29 | ...[...] | ArelInjection.rb:67:9:67:15 | user_id | provenance | | | PgInjection.rb:6:5:6:8 | name | PgInjection.rb:13:5:13:8 | qry1 : String | provenance | AdditionalTaintStep | | PgInjection.rb:6:5:6:8 | name | PgInjection.rb:19:5:19:8 | qry2 : String | provenance | AdditionalTaintStep | | PgInjection.rb:6:5:6:8 | name | PgInjection.rb:31:5:31:8 | qry3 : String | provenance | AdditionalTaintStep | @@ -235,37 +209,6 @@ nodes | ArelInjection.rb:4:12:4:29 | ...[...] | semmle.label | ...[...] | | ArelInjection.rb:6:20:6:61 | "SELECT * FROM users WHERE nam..." | semmle.label | "SELECT * FROM users WHERE nam..." | | ArelInjection.rb:7:39:7:80 | "SELECT * FROM users WHERE nam..." | semmle.label | "SELECT * FROM users WHERE nam..." | -| ArelInjection.rb:13:5:13:8 | name | semmle.label | name | -| ArelInjection.rb:13:12:13:17 | call to params | semmle.label | call to params | -| ArelInjection.rb:13:12:13:29 | ...[...] | semmle.label | ...[...] | -| ArelInjection.rb:15:20:15:61 | "SELECT * FROM users WHERE nam..." | semmle.label | "SELECT * FROM users WHERE nam..." | -| ArelInjection.rb:16:39:16:80 | "SELECT * FROM users WHERE nam..." | semmle.label | "SELECT * FROM users WHERE nam..." | -| ArelInjection.rb:22:5:22:5 | x | semmle.label | x | -| ArelInjection.rb:22:9:22:14 | call to params | semmle.label | call to params | -| ArelInjection.rb:22:9:22:21 | ...[...] | semmle.label | ...[...] | -| ArelInjection.rb:23:14:23:52 | "SELECT * FROM users WHERE nam..." | semmle.label | "SELECT * FROM users WHERE nam..." | -| ArelInjection.rb:30:29:30:35 | user_id | semmle.label | user_id | -| ArelInjection.rb:32:18:32:60 | "SELECT * FROM users WHERE id ..." | semmle.label | "SELECT * FROM users WHERE id ..." | -| ArelInjection.rb:47:7:47:13 | user_id | semmle.label | user_id | -| ArelInjection.rb:47:17:47:22 | call to params | semmle.label | call to params | -| ArelInjection.rb:47:17:47:32 | ...[...] | semmle.label | ...[...] | -| ArelInjection.rb:48:7:48:14 | route_id | semmle.label | route_id | -| ArelInjection.rb:48:18:48:38 | call to route_param | semmle.label | call to route_param | -| ArelInjection.rb:49:7:49:10 | auth | semmle.label | auth | -| ArelInjection.rb:49:14:49:20 | call to headers | semmle.label | call to headers | -| ArelInjection.rb:49:14:49:36 | ...[...] | semmle.label | ...[...] | -| ArelInjection.rb:50:7:50:13 | session | semmle.label | session | -| ArelInjection.rb:50:17:50:23 | call to cookies | semmle.label | call to cookies | -| ArelInjection.rb:50:17:50:36 | ...[...] | semmle.label | ...[...] | -| ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | semmle.label | "SELECT * FROM users WHERE id ..." | -| ArelInjection.rb:59:7:59:13 | user_id | semmle.label | user_id | -| ArelInjection.rb:59:17:59:22 | call to params | semmle.label | call to params | -| ArelInjection.rb:59:17:59:32 | ...[...] | semmle.label | ...[...] | -| ArelInjection.rb:60:25:60:31 | user_id | semmle.label | user_id | -| ArelInjection.rb:67:9:67:15 | user_id | semmle.label | user_id | -| ArelInjection.rb:67:19:67:24 | call to params | semmle.label | call to params | -| ArelInjection.rb:67:19:67:29 | ...[...] | semmle.label | ...[...] | -| ArelInjection.rb:68:18:68:60 | "SELECT * FROM users WHERE id ..." | semmle.label | "SELECT * FROM users WHERE id ..." | | PgInjection.rb:6:5:6:8 | name | semmle.label | name | | PgInjection.rb:6:12:6:17 | call to params | semmle.label | call to params | | PgInjection.rb:6:12:6:24 | ...[...] | semmle.label | ...[...] | @@ -323,15 +266,6 @@ subpaths | ActiveRecordInjection.rb:216:38:216:53 | "role = #{...}" | ActiveRecordInjection.rb:222:29:222:34 | call to params | ActiveRecordInjection.rb:216:38:216:53 | "role = #{...}" | This SQL query depends on a $@. | ActiveRecordInjection.rb:222:29:222:34 | call to params | user-provided value | | ArelInjection.rb:6:20:6:61 | "SELECT * FROM users WHERE nam..." | ArelInjection.rb:4:12:4:17 | call to params | ArelInjection.rb:6:20:6:61 | "SELECT * FROM users WHERE nam..." | This SQL query depends on a $@. | ArelInjection.rb:4:12:4:17 | call to params | user-provided value | | ArelInjection.rb:7:39:7:80 | "SELECT * FROM users WHERE nam..." | ArelInjection.rb:4:12:4:17 | call to params | ArelInjection.rb:7:39:7:80 | "SELECT * FROM users WHERE nam..." | This SQL query depends on a $@. | ArelInjection.rb:4:12:4:17 | call to params | user-provided value | -| ArelInjection.rb:15:20:15:61 | "SELECT * FROM users WHERE nam..." | ArelInjection.rb:13:12:13:17 | call to params | ArelInjection.rb:15:20:15:61 | "SELECT * FROM users WHERE nam..." | This SQL query depends on a $@. | ArelInjection.rb:13:12:13:17 | call to params | user-provided value | -| ArelInjection.rb:16:39:16:80 | "SELECT * FROM users WHERE nam..." | ArelInjection.rb:13:12:13:17 | call to params | ArelInjection.rb:16:39:16:80 | "SELECT * FROM users WHERE nam..." | This SQL query depends on a $@. | ArelInjection.rb:13:12:13:17 | call to params | user-provided value | -| ArelInjection.rb:23:14:23:52 | "SELECT * FROM users WHERE nam..." | ArelInjection.rb:22:9:22:14 | call to params | ArelInjection.rb:23:14:23:52 | "SELECT * FROM users WHERE nam..." | This SQL query depends on a $@. | ArelInjection.rb:22:9:22:14 | call to params | user-provided value | -| ArelInjection.rb:32:18:32:60 | "SELECT * FROM users WHERE id ..." | ArelInjection.rb:59:17:59:22 | call to params | ArelInjection.rb:32:18:32:60 | "SELECT * FROM users WHERE id ..." | This SQL query depends on a $@. | ArelInjection.rb:59:17:59:22 | call to params | user-provided value | -| ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | ArelInjection.rb:47:17:47:22 | call to params | ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | This SQL query depends on a $@. | ArelInjection.rb:47:17:47:22 | call to params | user-provided value | -| ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | ArelInjection.rb:48:18:48:38 | call to route_param | ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | This SQL query depends on a $@. | ArelInjection.rb:48:18:48:38 | call to route_param | user-provided value | -| ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | ArelInjection.rb:49:14:49:20 | call to headers | ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | This SQL query depends on a $@. | ArelInjection.rb:49:14:49:20 | call to headers | user-provided value | -| ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | ArelInjection.rb:50:17:50:23 | call to cookies | ArelInjection.rb:54:16:54:153 | "SELECT * FROM users WHERE id ..." | This SQL query depends on a $@. | ArelInjection.rb:50:17:50:23 | call to cookies | user-provided value | -| ArelInjection.rb:68:18:68:60 | "SELECT * FROM users WHERE id ..." | ArelInjection.rb:67:19:67:24 | call to params | ArelInjection.rb:68:18:68:60 | "SELECT * FROM users WHERE id ..." | This SQL query depends on a $@. | ArelInjection.rb:67:19:67:24 | call to params | user-provided value | | PgInjection.rb:14:15:14:18 | qry1 | PgInjection.rb:6:12:6:17 | call to params | PgInjection.rb:14:15:14:18 | qry1 | This SQL query depends on a $@. | PgInjection.rb:6:12:6:17 | call to params | user-provided value | | PgInjection.rb:15:21:15:24 | qry1 | PgInjection.rb:6:12:6:17 | call to params | PgInjection.rb:15:21:15:24 | qry1 | This SQL query depends on a $@. | PgInjection.rb:6:12:6:17 | call to params | user-provided value | | PgInjection.rb:20:22:20:25 | qry2 | PgInjection.rb:6:12:6:17 | call to params | PgInjection.rb:20:22:20:25 | qry2 | This SQL query depends on a $@. | PgInjection.rb:6:12:6:17 | call to params | user-provided value | From 89e9ee43c00159a561bbad75b43296acefd87ef7 Mon Sep 17 00:00:00 2001 From: Chad Bentz <1760475+felickz@users.noreply.github.com> Date: Fri, 19 Sep 2025 18:28:45 -0400 Subject: [PATCH 10/19] Convert from GrapeHelperMethodTaintStep extends AdditionalTaintStep to a simplified GrapeHelperMethodTarget extends AdditionalCallTarget --- .../2025-09-15-grape-framework-support.md | 2 +- ruby/ql/lib/codeql/ruby/frameworks/Grape.qll | 39 +++++++------------ .../frameworks/grape/Flow.expected | 36 +++++++++++++++-- .../library-tests/frameworks/grape/app.rb | 4 +- 4 files changed, 49 insertions(+), 32 deletions(-) diff --git a/ruby/ql/lib/change-notes/2025-09-15-grape-framework-support.md b/ruby/ql/lib/change-notes/2025-09-15-grape-framework-support.md index 258da40d36c5..08ceed887f21 100644 --- a/ruby/ql/lib/change-notes/2025-09-15-grape-framework-support.md +++ b/ruby/ql/lib/change-notes/2025-09-15-grape-framework-support.md @@ -1,4 +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. \ No newline at end of file +* Initial modeling for the Ruby Grape framework in `Grape.qll` has been added to detect API endpoints, parameters, and headers within Grape API classes. \ No newline at end of file diff --git a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll index a1646b8654c9..31632e019485 100644 --- a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll +++ b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll @@ -3,6 +3,7 @@ */ 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 @@ -301,32 +302,20 @@ private class GrapeHelperMethod extends Method { } /** - * 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. + * 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 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 +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/Flow.expected b/ruby/ql/test/library-tests/frameworks/grape/Flow.expected index 0fd19d4eaced..c104b36afb2d 100644 --- a/ruby/ql/test/library-tests/frameworks/grape/Flow.expected +++ b/ruby/ql/test/library-tests/frameworks/grape/Flow.expected @@ -1,10 +1,15 @@ 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:70 | call to select | app.rb:149:21:149:31 | call to user_params | provenance | AdditionalTaintStep | -| app.rb:103:13:103:70 | call to select | app.rb:165:21:165:31 | call to user_params | provenance | AdditionalTaintStep | -| app.rb:107:13:107:32 | call to source | app.rb:143:18:143:43 | call to vulnerable_helper | provenance | AdditionalTaintStep | -| app.rb:111:13:111:33 | call to source | app.rb:150:25:150:37 | call to simple_helper | provenance | AdditionalTaintStep | +| 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:149:21:149:31 | call to user_params | provenance | | +| app.rb:103:13:103:70 | call to select | app.rb:165:21:165:31 | call to user_params | provenance | | +| app.rb:103:13:103:70 | call to select : [collection] [element] | app.rb:149:21:149:31 | call to user_params : [collection] [element] | provenance | | +| app.rb:103:13:103:70 | call to select : [collection] [element] | app.rb:165:21:165:31 | call to user_params : [collection] [element] | provenance | | +| app.rb:107:13:107:32 | call to source | app.rb:143:18:143:43 | call to vulnerable_helper | provenance | | +| app.rb:107:13:107:32 | call to source | app.rb:143:18:143:43 | call to vulnerable_helper | provenance | | +| app.rb:111:13:111:33 | call to source | app.rb:150:25:150:37 | call to simple_helper | provenance | | +| app.rb:111:13:111:33 | call to source | app.rb:150:25:150:37 | call to simple_helper | provenance | | | app.rb:126:9:126:15 | user_id | app.rb:133:14:133:20 | user_id | provenance | | | app.rb:126:19:126:24 | call to params | app.rb:126:19:126:34 | ...[...] | provenance | | | app.rb:126:19:126:34 | ...[...] | app.rb:126:9:126:15 | user_id | provenance | | @@ -17,20 +22,31 @@ edges | app.rb:129:19:129:25 | call to cookies | app.rb:129:19:129:38 | ...[...] | provenance | | | app.rb:129:19:129:38 | ...[...] | app.rb:129:9:129:15 | session | provenance | | | app.rb:143:9:143:14 | result | app.rb:144:14:144:19 | result | provenance | | +| app.rb:143:9:143:14 | result | app.rb:144:14:144:19 | result | provenance | | +| app.rb:143:18:143:43 | call to vulnerable_helper | app.rb:143:9:143:14 | result | provenance | | | app.rb:143:18:143:43 | call to vulnerable_helper | app.rb:143:9:143:14 | result | provenance | | | app.rb:149:9:149:17 | user_data | app.rb:151:14:151:22 | user_data | provenance | | +| app.rb:149:9:149:17 | user_data : [collection] [element] | app.rb:151:14:151:22 | user_data | provenance | | | app.rb:149:21:149:31 | call to user_params | app.rb:149:9:149:17 | user_data | provenance | | +| app.rb:149:21:149:31 | call to user_params : [collection] [element] | app.rb:149:9:149:17 | user_data : [collection] [element] | provenance | | | app.rb:150:9:150:21 | simple_result | app.rb:152:14:152:26 | simple_result | provenance | | +| app.rb:150:9:150:21 | simple_result | app.rb:152:14:152:26 | simple_result | provenance | | +| app.rb:150:25:150:37 | call to simple_helper | app.rb:150:9:150:21 | simple_result | provenance | | | app.rb:150:25:150:37 | call to simple_helper | app.rb:150:9:150:21 | simple_result | provenance | | | app.rb:159:13:159:19 | user_id | app.rb:160:18:160:24 | user_id | provenance | | | app.rb:159:23:159:28 | call to params | app.rb:159:23:159:33 | ...[...] | provenance | | | app.rb:159:23:159:33 | ...[...] | app.rb:159:13:159:19 | user_id | provenance | | | app.rb:165:9:165:17 | user_data | app.rb:166:14:166:22 | user_data | provenance | | +| app.rb:165:9:165:17 | user_data : [collection] [element] | app.rb:166:14:166:22 | user_data | provenance | | | app.rb:165:21:165:31 | call to user_params | app.rb:165:9:165:17 | user_data | provenance | | +| app.rb:165:21:165:31 | call to user_params : [collection] [element] | app.rb:165:9:165:17 | user_data : [collection] [element] | 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:126:9:126:15 | user_id | semmle.label | user_id | | app.rb:126:19:126:24 | call to params | semmle.label | call to params | @@ -48,20 +64,30 @@ nodes | app.rb:135:14:135:17 | auth | semmle.label | auth | | app.rb:136:14:136:20 | session | semmle.label | session | | app.rb:143:9:143:14 | result | semmle.label | result | +| app.rb:143:9:143:14 | result | semmle.label | result | | app.rb:143:18:143:43 | call to vulnerable_helper | semmle.label | call to vulnerable_helper | +| app.rb:143:18:143:43 | call to vulnerable_helper | semmle.label | call to vulnerable_helper | +| app.rb:144:14:144:19 | result | semmle.label | result | | app.rb:144:14:144:19 | result | semmle.label | result | | app.rb:149:9:149:17 | user_data | semmle.label | user_data | +| app.rb:149:9:149:17 | user_data : [collection] [element] | semmle.label | user_data : [collection] [element] | | app.rb:149:21:149:31 | call to user_params | semmle.label | call to user_params | +| app.rb:149:21:149:31 | call to user_params : [collection] [element] | semmle.label | call to user_params : [collection] [element] | | app.rb:150:9:150:21 | simple_result | semmle.label | simple_result | +| app.rb:150:9:150:21 | simple_result | semmle.label | simple_result | +| app.rb:150:25:150:37 | call to simple_helper | semmle.label | call to simple_helper | | app.rb:150:25:150:37 | call to simple_helper | semmle.label | call to simple_helper | | app.rb:151:14:151:22 | user_data | semmle.label | user_data | | app.rb:152:14:152:26 | simple_result | semmle.label | simple_result | +| app.rb:152:14:152:26 | simple_result | semmle.label | simple_result | | app.rb:159:13:159:19 | user_id | semmle.label | user_id | | app.rb:159:23:159:28 | call to params | semmle.label | call to params | | app.rb:159:23:159:33 | ...[...] | semmle.label | ...[...] | | app.rb:160:18:160:24 | user_id | semmle.label | user_id | | app.rb:165:9:165:17 | user_data | semmle.label | user_data | +| app.rb:165:9:165:17 | user_data : [collection] [element] | semmle.label | user_data : [collection] [element] | | app.rb:165:21:165:31 | call to user_params | semmle.label | call to user_params | +| app.rb:165:21:165:31 | call to user_params : [collection] [element] | semmle.label | call to user_params : [collection] [element] | | app.rb:166:14:166:22 | user_data | semmle.label | user_data | subpaths testFailures @@ -71,7 +97,9 @@ testFailures | app.rb:135:14:135:17 | auth | app.rb:128:16:128:22 | call to headers | app.rb:135:14:135:17 | auth | $@ | app.rb:128:16:128:22 | call to headers | call to headers | | app.rb:136:14:136:20 | session | app.rb:129:19:129:25 | call to cookies | app.rb:136:14:136:20 | session | $@ | app.rb:129:19:129:25 | call to cookies | call to cookies | | app.rb:144:14:144:19 | result | app.rb:107:13:107:32 | call to source | app.rb:144:14:144:19 | result | $@ | app.rb:107:13:107:32 | call to source | call to source | +| app.rb:144:14:144:19 | result | app.rb:107:13:107:32 | call to source | app.rb:144:14:144:19 | result | $@ | app.rb:107:13:107:32 | call to source | call to source | | app.rb:151:14:151:22 | user_data | app.rb:103:13:103:18 | call to params | app.rb:151:14:151:22 | user_data | $@ | app.rb:103:13:103:18 | call to params | call to params | | app.rb:152:14:152:26 | simple_result | app.rb:111:13:111:33 | call to source | app.rb:152:14:152:26 | simple_result | $@ | app.rb:111:13:111:33 | call to source | call to source | +| app.rb:152:14:152:26 | simple_result | app.rb:111:13:111:33 | call to source | app.rb:152:14:152:26 | simple_result | $@ | app.rb:111:13:111:33 | call to source | call to source | | app.rb:160:18:160:24 | user_id | app.rb:159:23:159:28 | call to params | app.rb:160:18:160:24 | user_id | $@ | app.rb:159:23:159:28 | call to params | call to params | | app.rb:166:14:166:22 | user_data | app.rb:103:13:103:18 | call to params | app.rb:166:14:166:22 | user_data | $@ | app.rb:103:13:103:18 | call to params | call to params | diff --git a/ruby/ql/test/library-tests/frameworks/grape/app.rb b/ruby/ql/test/library-tests/frameworks/grape/app.rb index 6fbb184cab98..81f464826876 100644 --- a/ruby/ql/test/library-tests/frameworks/grape/app.rb +++ b/ruby/ql/test/library-tests/frameworks/grape/app.rb @@ -141,7 +141,7 @@ def simple_helper # Test helper method parameter passing dataflow user_id = params[:user_id] result = vulnerable_helper(user_id) - sink result # $ hasTaintFlow=paramHelper + sink result # $ hasValueFlow=paramHelper end post '/users' do @@ -149,7 +149,7 @@ def simple_helper user_data = user_params simple_result = simple_helper sink user_data # $ hasTaintFlow - sink simple_result # $ hasTaintFlow=simpleHelper + sink simple_result # $ hasValueFlow=simpleHelper end # Test route_param block pattern 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 11/19] Refactor Grape framework to be encapsulated properly in Module --- ruby/ql/lib/codeql/ruby/frameworks/Grape.qll | 480 +++++++++--------- .../library-tests/frameworks/grape/Grape.ql | 14 +- 2 files changed, 250 insertions(+), 244 deletions(-) diff --git a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll index 31632e019485..0999be945052 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) - } /** - * Gets a `GrapeEndpoint` defined in this class. + * 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 + * ``` */ - GrapeEndpoint getAnEndpoint() { result.getApiClass() = this } + class GrapeApiClass extends DataFlow::ClassNode { + GrapeApiClass() { + this = grapeApiBaseClass().getADescendentModule() and + not exists(DataFlow::ModuleNode m | m = grapeApiBaseClass().asModule() | this = m) + } - /** - * 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 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 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 { - private GrapeApiClass apiClass; + /** + * 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"]) + } - 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 the HTTP method for this endpoint (e.g., "GET", "POST", etc.) + * A `RemoteFlowSource::Range` to represent accessing the + * Grape parameters available via the `params` method within an endpoint. */ - string getHttpMethod() { result = this.getMethodName().toUpperCase() } + class GrapeParamsSource extends Http::Server::RequestInputAccess::Range { + GrapeParamsSource() { this.asExpr().getExpr() instanceof GrapeParamsCall } - /** - * Gets the API class containing this endpoint. - */ - GrapeApiClass getApiClass() { result = apiClass } + override string getSourceType() { result = "Grape::API#params" } + + override Http::Server::RequestInputKind getKind() { + result = Http::Server::parameterInputKind() + } + } /** - * Gets the block containing the endpoint logic. + * A call to `params` from within a Grape API endpoint or helper method. */ - DataFlow::BlockNode getBody() { result = this.getBlock() } + 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 path pattern for this endpoint, if specified. + * A call to `headers` from within a Grape API endpoint or headers block. + * Headers can also be a source of user input. */ - 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 } + 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#params" } + override string getSourceType() { result = "Grape::API#headers" } - 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() - ) + override Http::Server::RequestInputKind getKind() { result = Http::Server::headerInputKind() } } -} -/** - * 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 + /** + * 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() + } } - override string getSourceType() { result = "Grape::API#headers" } + /** + * 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 Http::Server::RequestInputKind getKind() { result = Http::Server::headerInputKind() } -} + override string getSourceType() { result = "Grape::API#request" } -/** - * 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() + override Http::Server::RequestInputKind getKind() { + result = Http::Server::parameterInputKind() + } } -} -/** - * 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 } + /** + * 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 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() + override Http::Server::RequestInputKind getKind() { + result = Http::Server::parameterInputKind() + } } -} -/** - * 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 `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 `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 `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 `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 `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` 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 + /** + * 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()) + ) + } } - override string getSourceType() { result = "Grape::API#cookies" } + /** + * 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 Http::Server::RequestInputKind getKind() { result = Http::Server::cookieInputKind() } -} + override string getSourceType() { result = "Grape::API#cookies" } -/** - * 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() + override Http::Server::RequestInputKind getKind() { result = Http::Server::cookieInputKind() } } -} -/** - * 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() - ) + /** + * 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() + } } /** - * Gets the API class that contains this helper method. + * A method defined within a `helpers` block in a Grape API class. + * These methods become available in endpoint contexts through Grape's DSL. */ - GrapeApiClass getApiClass() { result = apiClass } -} + private class GrapeHelperMethod extends Method { + private GrapeApiClass apiClass; + + GrapeHelperMethod() { + exists(DataFlow::CallNode helpersCall | + helpersCall = apiClass.getAModuleLevelCall("helpers") and + this.getParent+() = helpersCall.getBlock().asExpr().getExpr() + ) + } -/** - * 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() - ) + /** + * 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 c9aa7c29082c..c5f0798f7a6b 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() } From 50bf9ae7563e671814820819269200980cb5a5b3 Mon Sep 17 00:00:00 2001 From: Chad Bentz <1760475+felickz@users.noreply.github.com> Date: Sun, 21 Sep 2025 20:44:46 -0400 Subject: [PATCH 12/19] Refactor RootApi class to use getAnImmediateDescendent for clarity --- ruby/ql/lib/codeql/ruby/frameworks/Grape.qll | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll index 0999be945052..ce0b47502f96 100644 --- a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll +++ b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll @@ -26,7 +26,7 @@ module Grape { */ class RootApi extends GrapeApiClass { RootApi() { - not exists(GrapeApiClass parent | this != parent and this = parent.getADescendent()) + not this = any(GrapeApiClass parent).getAnImmediateDescendent() } } From 1bf6101967e860043695e2d93e10c34373ea8b39 Mon Sep 17 00:00:00 2001 From: Chad Bentz <1760475+felickz@users.noreply.github.com> Date: Sun, 21 Sep 2025 20:52:28 -0400 Subject: [PATCH 13/19] Remove redundant exclusion of base Grape::API module from GrapeApiClass - should not impact extracted application code --- ruby/ql/lib/codeql/ruby/frameworks/Grape.qll | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll index ce0b47502f96..4e178792572f 100644 --- a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll +++ b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll @@ -45,8 +45,7 @@ module Grape { */ class GrapeApiClass extends DataFlow::ClassNode { GrapeApiClass() { - this = grapeApiBaseClass().getADescendentModule() and - not exists(DataFlow::ModuleNode m | m = grapeApiBaseClass().asModule() | this = m) + this = grapeApiBaseClass().getADescendentModule() } /** From b837c56bec5a598bdd951e30e998e449f8f19305 Mon Sep 17 00:00:00 2001 From: Chad Bentz <1760475+felickz@users.noreply.github.com> Date: Mon, 22 Sep 2025 10:13:33 -0400 Subject: [PATCH 14/19] Refactor RootApi and GrapeApiClass constructors for improved readability; add getHelperSelf method to retrieve self parameter in helpers block. --- ruby/ql/lib/codeql/ruby/frameworks/Grape.qll | 39 +++++++++++--------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll index 4e178792572f..95aa42fdfad7 100644 --- a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll +++ b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll @@ -25,9 +25,7 @@ module Grape { * 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() - } + RootApi() { not this = any(GrapeApiClass parent).getAnImmediateDescendent() } } /** @@ -44,9 +42,7 @@ module Grape { * ``` */ class GrapeApiClass extends DataFlow::ClassNode { - GrapeApiClass() { - this = grapeApiBaseClass().getADescendentModule() - } + GrapeApiClass() { this = grapeApiBaseClass().getADescendentModule() } /** * Gets a `GrapeEndpoint` defined in this class. @@ -63,6 +59,20 @@ module Grape { // 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() { @@ -122,17 +132,12 @@ module Grape { */ 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() + 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() ) } } From ecd0ce65fe91f6ff604185c59f526ca0442c8389 Mon Sep 17 00:00:00 2001 From: Chad Bentz <1760475+felickz@users.noreply.github.com> Date: Mon, 22 Sep 2025 12:52:30 -0400 Subject: [PATCH 15/19] Refactor GrapeHeadersBlockCall and GrapeCookiesBlockCall to simplify method call checks --- ruby/ql/lib/codeql/ruby/frameworks/Grape.qll | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll index 95aa42fdfad7..7e3d6c54fe45 100644 --- a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll +++ b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll @@ -237,11 +237,8 @@ module Grape { */ private class GrapeHeadersBlockCall extends MethodCall { GrapeHeadersBlockCall() { - exists(GrapeApiClass api | - this.getParent+() = api.getADeclaration() and - this.getMethodName() = "headers" and - exists(this.getBlock()) - ) + this = grapeApiInstance().getAMethodCall("headers").asExpr().getExpr() and + exists(this.getBlock()) } } @@ -251,11 +248,8 @@ module Grape { */ private class GrapeCookiesBlockCall extends MethodCall { GrapeCookiesBlockCall() { - exists(GrapeApiClass api | - this.getParent+() = api.getADeclaration() and - this.getMethodName() = "cookies" and - exists(this.getBlock()) - ) + this = grapeApiInstance().getAMethodCall("cookies").asExpr().getExpr() and + exists(this.getBlock()) } } From 0665c39a072181e9adac06470466ff4377b17148 Mon Sep 17 00:00:00 2001 From: Chad Bentz <1760475+felickz@users.noreply.github.com> Date: Mon, 22 Sep 2025 19:08:34 -0400 Subject: [PATCH 16/19] Refactor GrapeHelperMethod constructor to reuse getHelperSelf to traverse dataflow instead of AST - add tests to check for nested helpers --- ruby/ql/lib/codeql/ruby/frameworks/Grape.qll | 7 +- .../frameworks/grape/Flow.expected | 276 ++++++++++++------ .../frameworks/grape/Grape.expected | 32 +- .../library-tests/frameworks/grape/app.rb | 71 +++++ 4 files changed, 275 insertions(+), 111 deletions(-) diff --git a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll index 7e3d6c54fe45..4d64e9461b38 100644 --- a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll +++ b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll @@ -291,12 +291,7 @@ module Grape { private class GrapeHelperMethod extends Method { private GrapeApiClass apiClass; - GrapeHelperMethod() { - exists(DataFlow::CallNode helpersCall | - helpersCall = apiClass.getAModuleLevelCall("helpers") and - this.getParent+() = helpersCall.getBlock().asExpr().getExpr() - ) - } + GrapeHelperMethod() { this = apiClass.getHelperSelf().getSelfVariable().getDeclaringScope() } /** * Gets the API class that contains this helper method. diff --git a/ruby/ql/test/library-tests/frameworks/grape/Flow.expected b/ruby/ql/test/library-tests/frameworks/grape/Flow.expected index c104b36afb2d..f04bd930ea97 100644 --- a/ruby/ql/test/library-tests/frameworks/grape/Flow.expected +++ b/ruby/ql/test/library-tests/frameworks/grape/Flow.expected @@ -2,44 +2,80 @@ 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:149:21:149:31 | call to user_params | provenance | | -| app.rb:103:13:103:70 | call to select | app.rb:165:21:165:31 | call to user_params | provenance | | -| app.rb:103:13:103:70 | call to select : [collection] [element] | app.rb:149:21:149:31 | call to user_params : [collection] [element] | provenance | | -| app.rb:103:13:103:70 | call to select : [collection] [element] | app.rb:165:21:165:31 | call to user_params : [collection] [element] | provenance | | -| app.rb:107:13:107:32 | call to source | app.rb:143:18:143:43 | call to vulnerable_helper | provenance | | -| app.rb:107:13:107:32 | call to source | app.rb:143:18:143:43 | call to vulnerable_helper | provenance | | -| app.rb:111:13:111:33 | call to source | app.rb:150:25:150:37 | call to simple_helper | provenance | | -| app.rb:111:13:111:33 | call to source | app.rb:150:25:150:37 | call to simple_helper | provenance | | -| app.rb:126:9:126:15 | user_id | app.rb:133:14:133:20 | user_id | provenance | | -| app.rb:126:19:126:24 | call to params | app.rb:126:19:126:34 | ...[...] | provenance | | -| app.rb:126:19:126:34 | ...[...] | app.rb:126:9:126:15 | user_id | provenance | | -| app.rb:127:9:127:16 | route_id | app.rb:134:14:134:21 | route_id | provenance | | -| app.rb:127:20:127:40 | call to route_param | app.rb:127:9:127:16 | route_id | provenance | | -| app.rb:128:9:128:12 | auth | app.rb:135:14:135:17 | auth | provenance | | -| app.rb:128:16:128:22 | call to headers | app.rb:128:16:128:38 | ...[...] | provenance | | -| app.rb:128:16:128:38 | ...[...] | app.rb:128:9:128:12 | auth | provenance | | -| app.rb:129:9:129:15 | session | app.rb:136:14:136:20 | session | provenance | | -| app.rb:129:19:129:25 | call to cookies | app.rb:129:19:129:38 | ...[...] | provenance | | -| app.rb:129:19:129:38 | ...[...] | app.rb:129:9:129:15 | session | provenance | | -| app.rb:143:9:143:14 | result | app.rb:144:14:144:19 | result | provenance | | -| app.rb:143:9:143:14 | result | app.rb:144:14:144:19 | result | provenance | | -| app.rb:143:18:143:43 | call to vulnerable_helper | app.rb:143:9:143:14 | result | provenance | | -| app.rb:143:18:143:43 | call to vulnerable_helper | app.rb:143:9:143:14 | result | provenance | | -| app.rb:149:9:149:17 | user_data | app.rb:151:14:151:22 | user_data | provenance | | -| app.rb:149:9:149:17 | user_data : [collection] [element] | app.rb:151:14:151:22 | user_data | provenance | | -| app.rb:149:21:149:31 | call to user_params | app.rb:149:9:149:17 | user_data | provenance | | -| app.rb:149:21:149:31 | call to user_params : [collection] [element] | app.rb:149:9:149:17 | user_data : [collection] [element] | provenance | | -| app.rb:150:9:150:21 | simple_result | app.rb:152:14:152:26 | simple_result | provenance | | -| app.rb:150:9:150:21 | simple_result | app.rb:152:14:152:26 | simple_result | provenance | | -| app.rb:150:25:150:37 | call to simple_helper | app.rb:150:9:150:21 | simple_result | provenance | | -| app.rb:150:25:150:37 | call to simple_helper | app.rb:150:9:150:21 | simple_result | provenance | | -| app.rb:159:13:159:19 | user_id | app.rb:160:18:160:24 | user_id | provenance | | -| app.rb:159:23:159:28 | call to params | app.rb:159:23:159:33 | ...[...] | provenance | | -| app.rb:159:23:159:33 | ...[...] | app.rb:159:13:159:19 | user_id | provenance | | -| app.rb:165:9:165:17 | user_data | app.rb:166:14:166:22 | user_data | provenance | | -| app.rb:165:9:165:17 | user_data : [collection] [element] | app.rb:166:14:166:22 | user_data | provenance | | -| app.rb:165:21:165:31 | call to user_params | app.rb:165:9:165:17 | user_data | provenance | | -| app.rb:165:21:165:31 | call to user_params : [collection] [element] | app.rb:165:9:165:17 | user_data : [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 | @@ -48,58 +84,118 @@ nodes | 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:126:9:126:15 | user_id | semmle.label | user_id | -| app.rb:126:19:126:24 | call to params | semmle.label | call to params | -| app.rb:126:19:126:34 | ...[...] | semmle.label | ...[...] | -| app.rb:127:9:127:16 | route_id | semmle.label | route_id | -| app.rb:127:20:127:40 | call to route_param | semmle.label | call to route_param | -| app.rb:128:9:128:12 | auth | semmle.label | auth | -| app.rb:128:16:128:22 | call to headers | semmle.label | call to headers | -| app.rb:128:16:128:38 | ...[...] | semmle.label | ...[...] | -| app.rb:129:9:129:15 | session | semmle.label | session | -| app.rb:129:19:129:25 | call to cookies | semmle.label | call to cookies | -| app.rb:129:19:129:38 | ...[...] | semmle.label | ...[...] | -| app.rb:133:14:133:20 | user_id | semmle.label | user_id | -| app.rb:134:14:134:21 | route_id | semmle.label | route_id | -| app.rb:135:14:135:17 | auth | semmle.label | auth | -| app.rb:136:14:136:20 | session | semmle.label | session | -| app.rb:143:9:143:14 | result | semmle.label | result | -| app.rb:143:9:143:14 | result | semmle.label | result | -| app.rb:143:18:143:43 | call to vulnerable_helper | semmle.label | call to vulnerable_helper | -| app.rb:143:18:143:43 | call to vulnerable_helper | semmle.label | call to vulnerable_helper | -| app.rb:144:14:144:19 | result | semmle.label | result | -| app.rb:144:14:144:19 | result | semmle.label | result | -| app.rb:149:9:149:17 | user_data | semmle.label | user_data | -| app.rb:149:9:149:17 | user_data : [collection] [element] | semmle.label | user_data : [collection] [element] | -| app.rb:149:21:149:31 | call to user_params | semmle.label | call to user_params | -| app.rb:149:21:149:31 | call to user_params : [collection] [element] | semmle.label | call to user_params : [collection] [element] | -| app.rb:150:9:150:21 | simple_result | semmle.label | simple_result | -| app.rb:150:9:150:21 | simple_result | semmle.label | simple_result | -| app.rb:150:25:150:37 | call to simple_helper | semmle.label | call to simple_helper | -| app.rb:150:25:150:37 | call to simple_helper | semmle.label | call to simple_helper | -| app.rb:151:14:151:22 | user_data | semmle.label | user_data | -| app.rb:152:14:152:26 | simple_result | semmle.label | simple_result | -| app.rb:152:14:152:26 | simple_result | semmle.label | simple_result | -| app.rb:159:13:159:19 | user_id | semmle.label | user_id | -| app.rb:159:23:159:28 | call to params | semmle.label | call to params | -| app.rb:159:23:159:33 | ...[...] | semmle.label | ...[...] | -| app.rb:160:18:160:24 | user_id | semmle.label | user_id | -| app.rb:165:9:165:17 | user_data | semmle.label | user_data | -| app.rb:165:9:165:17 | user_data : [collection] [element] | semmle.label | user_data : [collection] [element] | -| app.rb:165:21:165:31 | call to user_params | semmle.label | call to user_params | -| app.rb:165:21:165:31 | call to user_params : [collection] [element] | semmle.label | call to user_params : [collection] [element] | -| app.rb:166:14:166:22 | user_data | semmle.label | user_data | +| 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:133:14:133:20 | user_id | app.rb:126:19:126:24 | call to params | app.rb:133:14:133:20 | user_id | $@ | app.rb:126:19:126:24 | call to params | call to params | -| app.rb:134:14:134:21 | route_id | app.rb:127:20:127:40 | call to route_param | app.rb:134:14:134:21 | route_id | $@ | app.rb:127:20:127:40 | call to route_param | call to route_param | -| app.rb:135:14:135:17 | auth | app.rb:128:16:128:22 | call to headers | app.rb:135:14:135:17 | auth | $@ | app.rb:128:16:128:22 | call to headers | call to headers | -| app.rb:136:14:136:20 | session | app.rb:129:19:129:25 | call to cookies | app.rb:136:14:136:20 | session | $@ | app.rb:129:19:129:25 | call to cookies | call to cookies | -| app.rb:144:14:144:19 | result | app.rb:107:13:107:32 | call to source | app.rb:144:14:144:19 | result | $@ | app.rb:107:13:107:32 | call to source | call to source | -| app.rb:144:14:144:19 | result | app.rb:107:13:107:32 | call to source | app.rb:144:14:144:19 | result | $@ | app.rb:107:13:107:32 | call to source | call to source | -| app.rb:151:14:151:22 | user_data | app.rb:103:13:103:18 | call to params | app.rb:151:14:151:22 | user_data | $@ | app.rb:103:13:103:18 | call to params | call to params | -| app.rb:152:14:152:26 | simple_result | app.rb:111:13:111:33 | call to source | app.rb:152:14:152:26 | simple_result | $@ | app.rb:111:13:111:33 | call to source | call to source | -| app.rb:152:14:152:26 | simple_result | app.rb:111:13:111:33 | call to source | app.rb:152:14:152:26 | simple_result | $@ | app.rb:111:13:111:33 | call to source | call to source | -| app.rb:160:18:160:24 | user_id | app.rb:159:23:159:28 | call to params | app.rb:160:18:160:24 | user_id | $@ | app.rb:159:23:159:28 | call to params | call to params | -| app.rb:166:14:166:22 | user_data | app.rb:103:13:103:18 | call to params | app.rb:166:14:166:22 | user_data | $@ | app.rb:103:13:103:18 | call to params | call to params | +| 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 | diff --git a/ruby/ql/test/library-tests/frameworks/grape/Grape.expected b/ruby/ql/test/library-tests/frameworks/grape/Grape.expected index d39d9430f926..7088eeb9018a 100644 --- a/ruby/ql/test/library-tests/frameworks/grape/Grape.expected +++ b/ruby/ql/test/library-tests/frameworks/grape/Grape.expected @@ -1,7 +1,7 @@ grapeApiClasses | app.rb:1:1:90:3 | MyAPI | | app.rb:92:1:96:3 | AdminAPI | -| app.rb:98:1:168:3 | UserAPI | +| 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 | @@ -14,10 +14,11 @@ grapeEndpoints | 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:168:3 | UserAPI | app.rb:124:5:138:7 | call to get | GET | /comprehensive_test/:user_id | -| app.rb:98:1:168:3 | UserAPI | app.rb:140:5:145:7 | call to get | GET | /helper_test/:user_id | -| app.rb:98:1:168:3 | UserAPI | app.rb:147:5:153:7 | call to post | POST | /users | -| app.rb:98:1:168:3 | UserAPI | app.rb:164:5:167:7 | call to post | POST | /users | +| 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 | @@ -28,29 +29,30 @@ grapeParams | 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:126:19:126:24 | call to params | -| app.rb:142:19:142:24 | call to params | -| app.rb:159:23:159:28 | 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:116:5:118:7 | call to headers | -| app.rb:128:16:128:22 | 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:130:21:130:27 | 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:127:20:127:40 | call to route_param | -| app.rb:156:5:162:7 | 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:120:5:122:7 | call to cookies | -| app.rb:129:19:129:25 | call to cookies | +| app.rb:160:5:162:7 | call to cookies | +| app.rb:169:19:169:25 | call to cookies | diff --git a/ruby/ql/test/library-tests/frameworks/grape/app.rb b/ruby/ql/test/library-tests/frameworks/grape/app.rb index 81f464826876..1b1fd15d5d8c 100644 --- a/ruby/ql/test/library-tests/frameworks/grape/app.rb +++ b/ruby/ql/test/library-tests/frameworks/grape/app.rb @@ -110,6 +110,46 @@ def vulnerable_helper(user_id) 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 @@ -165,4 +205,35 @@ def simple_helper 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 From 6e56c549b2600540306812048ead37ed53dd37bc Mon Sep 17 00:00:00 2001 From: Chad Bentz <1760475+felickz@users.noreply.github.com> Date: Mon, 22 Sep 2025 19:21:23 -0400 Subject: [PATCH 17/19] Refactor Grape method call classes to simplify handling of API instance calls for headers, request, route_param, and cookies --- ruby/ql/lib/codeql/ruby/frameworks/Grape.qll | 28 +++----------------- 1 file changed, 4 insertions(+), 24 deletions(-) diff --git a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll index 4d64e9461b38..9b7ae6185cdb 100644 --- a/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll +++ b/ruby/ql/lib/codeql/ruby/frameworks/Grape.qll @@ -163,12 +163,7 @@ module Grape { */ 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 + // Handle cases where headers is called on an instance of a Grape API class this = grapeApiInstance().getAMethodCall("headers").asExpr().getExpr() } } @@ -206,12 +201,7 @@ module Grape { */ 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 + // Handle cases where request is called on an instance of a Grape API class this = grapeApiInstance().getAMethodCall("request").asExpr().getExpr() } } @@ -221,12 +211,7 @@ module Grape { */ 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 + // Handle cases where route_param is called on an instance of a Grape API class this = grapeApiInstance().getAMethodCall("route_param").asExpr().getExpr() } } @@ -274,12 +259,7 @@ module Grape { */ 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 + // Handle cases where cookies is called on an instance of a Grape API class this = grapeApiInstance().getAMethodCall("cookies").asExpr().getExpr() } } From 89fd9694cef692446e54e71a730abf1d427b7627 Mon Sep 17 00:00:00 2001 From: Chad Bentz <1760475+felickz@users.noreply.github.com> Date: Mon, 22 Sep 2025 19:25:05 -0400 Subject: [PATCH 18/19] codeql query format --- ruby/ql/test/library-tests/frameworks/grape/Grape.ql | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ruby/ql/test/library-tests/frameworks/grape/Grape.ql b/ruby/ql/test/library-tests/frameworks/grape/Grape.ql index c5f0798f7a6b..63cd15a547e1 100644 --- a/ruby/ql/test/library-tests/frameworks/grape/Grape.ql +++ b/ruby/ql/test/library-tests/frameworks/grape/Grape.ql @@ -5,7 +5,9 @@ import codeql.ruby.AST query predicate grapeApiClasses(Grape::GrapeApiClass api) { any() } -query predicate grapeEndpoints(Grape::GrapeApiClass api, Grape::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() From 37e0c3084278fd5a326b7021eaf73b08d1be801f Mon Sep 17 00:00:00 2001 From: Chad Bentz <1760475+felickz@users.noreply.github.com> Date: Tue, 23 Sep 2025 10:40:30 -0400 Subject: [PATCH 19/19] Add expected output for VariablesConsistency test case --- .../grape/CONSISTENCY/VariablesConsistency.expected | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 ruby/ql/test/library-tests/frameworks/grape/CONSISTENCY/VariablesConsistency.expected diff --git a/ruby/ql/test/library-tests/frameworks/grape/CONSISTENCY/VariablesConsistency.expected b/ruby/ql/test/library-tests/frameworks/grape/CONSISTENCY/VariablesConsistency.expected new file mode 100644 index 000000000000..edcc754b792e --- /dev/null +++ b/ruby/ql/test/library-tests/frameworks/grape/CONSISTENCY/VariablesConsistency.expected @@ -0,0 +1,4 @@ +variableIsCaptured +| app.rb:126:9:130:11 | self | CapturedVariable is not captured | +consistencyOverview +| CapturedVariable is not captured | 1 |