Ruby: Recognise rack applications

This is a basic first step in modelling rack apps. We recognise classes
that look like rack applications and then treat the argument to `call`
in the same way that we treat `request.env` in ActionController classes.

This finds a TP in CVE-2021-43840.
This commit is contained in:
Harry Maclean
2023-01-12 11:28:31 +13:00
parent ce06df3152
commit 0626d693f5
7 changed files with 120 additions and 17 deletions

View File

@@ -16,6 +16,7 @@ private import codeql.ruby.frameworks.ActiveSupport
private import codeql.ruby.frameworks.Archive
private import codeql.ruby.frameworks.Arel
private import codeql.ruby.frameworks.GraphQL
private import codeql.ruby.frameworks.Rack
private import codeql.ruby.frameworks.Rails
private import codeql.ruby.frameworks.Railties
private import codeql.ruby.frameworks.Stdlib

View File

@@ -1002,6 +1002,9 @@ class CallableNode extends ExprNode {
/** Gets the `n`th positional parameter. */
ParameterNode getParameter(int n) { this.getParameterPosition(result).isPositional(n) }
/** Gets the number of parameters of this callable. */
final int getNumberOfParameters() { result = count(this.getParameter(_)) }
/** Gets the keyword parameter of the given name. */
ParameterNode getKeywordParameter(string name) {
this.getParameterPosition(result).isKeyword(name)

View File

@@ -301,27 +301,39 @@ private module Request {
override Http::Server::RequestInputKind getKind() { result = Http::Server::bodyInputKind() }
}
/**
* A method call on `request` which returns the rack env.
* This is a hash containing all the information about the request. Values
* under keys starting with `HTTP_` are user-controlled.
*/
private class EnvCall extends RequestMethodCall {
EnvCall() { this.getMethodName() = ["env", "filtered_env"] }
}
private module Env {
abstract private class Env extends DataFlow::LocalSourceNode { }
/**
* A read of a user-controlled parameter from the request env.
*/
private class EnvHttpAccess extends DataFlow::CallNode, Http::Server::RequestInputAccess::Range {
EnvHttpAccess() {
this = any(EnvCall c).getAMethodCall("[]") and
this.getArgument(0).getConstantValue().getString().regexpMatch("^HTTP_.+")
/**
* A method call on `request` which returns the rack env.
* This is a hash containing all the information about the request. Values
* under keys starting with `HTTP_` are user-controlled.
*/
private class RequestEnvCall extends DataFlow::CallNode, Env {
RequestEnvCall() { this.getMethodName() = ["env", "filtered_env"] }
}
override Http::Server::RequestInputKind getKind() { result = Http::Server::headerInputKind() }
private import codeql.ruby.frameworks.Rack
override string getSourceType() { result = "ActionDispatch::Request#env[]" }
private class RackEnv extends Env {
RackEnv() { this = any(Rack::AppCandidate app).getEnv().getALocalUse() }
}
/**
* A read of a user-controlled parameter from the request env.
*/
private class EnvHttpAccess extends DataFlow::CallNode, Http::Server::RequestInputAccess::Range {
EnvHttpAccess() {
this = any(Env c).getAMethodCall("[]") and
exists(string key | key = this.getArgument(0).getConstantValue().getString() |
key.regexpMatch("^HTTP_.+") or key = "PATH_INFO"
)
}
override Http::Server::RequestInputKind getKind() { result = Http::Server::headerInputKind() }
override string getSourceType() { result = "ActionDispatch::Request#env[]" }
}
}
}

View File

@@ -0,0 +1,37 @@
/**
* Provides modeling for the Rack library.
*/
private import codeql.ruby.controlflow.CfgNodes::ExprNodes
private import codeql.ruby.DataFlow
/**
* Provides modeling for the Rack library.
*/
module Rack {
/**
* A class that may be a rack application.
* This is a class that has a `call` method that takes a single argument
* (traditionally called `env`) and returns a rack-compatible response.
*/
class AppCandidate extends DataFlow::ClassNode {
private DataFlow::MethodNode call;
AppCandidate() {
call = this.getInstanceMethod("call") and
call.getNumberOfParameters() = 1 and
isRackResponse(call.getAReturningNode())
}
DataFlow::ParameterNode getEnv() { result = call.getParameter(0) }
}
private predicate isRackResponse(DataFlow::Node r) {
// [status, headers, body]
exists(ArrayLiteralCfgNode arr | arr.getNumberOfArguments() = 3 |
r.asExpr() = arr
or
exists(DataFlow::LocalSourceNode n | n.asExpr() = arr | n.flowsTo(r))
)
}
}

View File

@@ -0,0 +1,3 @@
| rack.rb:1:1:5:3 | HelloWorld | rack.rb:2:12:2:14 | env |
| rack.rb:7:1:16:3 | Proxy | rack.rb:12:12:12:18 | the_env |
| rack.rb:18:1:31:3 | Logger | rack.rb:24:12:24:14 | env |

View File

@@ -0,0 +1,4 @@
private import codeql.ruby.frameworks.Rack
private import codeql.ruby.DataFlow
query predicate rackApps(Rack::AppCandidate c, DataFlow::ParameterNode env) { env = c.getEnv() }

View File

@@ -0,0 +1,43 @@
class HelloWorld
def call(env)
[200, {'Content-Type' => 'text/plain'}, ['Hello World']]
end
end
class Proxy
def initialize(app)
@app = app
end
def call(the_env)
status, headers, body = @app.call(the_env)
[status, headers, body]
end
end
class Logger
def initialize(app, logger = nil)
@app = app
@logger = logger
end
def call(env)
began_at = Utils.clock_time
status, header, body = @app.call(env)
header = Utils::HeaderHash.new(header)
body = BodyProxy.new(body) { log(env, status, header, began_at) }
[status, header, body]
end
end
class Foo
def not_call(env)
[1, 2, 3]
end
end
class Bar
def call(env)
nil
end
end