mirror of
https://github.com/github/codeql.git
synced 2026-04-26 17:25:19 +02:00
Add initial support for Ruby Grape
This commit is contained in:
@@ -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
|
||||
|
||||
198
ruby/ql/lib/codeql/ruby/frameworks/Grape.qll
Normal file
198
ruby/ql/lib/codeql/ruby/frameworks/Grape.qll
Normal file
@@ -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()
|
||||
}
|
||||
}
|
||||
25
ruby/ql/test/library-tests/frameworks/grape/Grape.expected
Normal file
25
ruby/ql/test/library-tests/frameworks/grape/Grape.expected
Normal file
@@ -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 |
|
||||
18
ruby/ql/test/library-tests/frameworks/grape/Grape.ql
Normal file
18
ruby/ql/test/library-tests/frameworks/grape/Grape.ql
Normal file
@@ -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() }
|
||||
54
ruby/ql/test/library-tests/frameworks/grape/app.rb
Normal file
54
ruby/ql/test/library-tests/frameworks/grape/app.rb
Normal file
@@ -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
|
||||
@@ -6,4 +6,13 @@ class PotatoController < ActionController::Base
|
||||
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
|
||||
@@ -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 |
|
||||
|
||||
Reference in New Issue
Block a user