Skip to content

Commit 07a7a21

Browse files
authored
Merge pull request github#11871 from hmac/rack
2 parents 1fcfae2 + e6e4e29 commit 07a7a21

File tree

8 files changed

+154
-17
lines changed

8 files changed

+154
-17
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: minorAnalysis
3+
---
4+
* Access to headers stored in the `env` of Rack requests is now recognized as a source of remote input.

ruby/ql/lib/codeql/ruby/Frameworks.qll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ private import codeql.ruby.frameworks.ActiveSupport
1616
private import codeql.ruby.frameworks.Archive
1717
private import codeql.ruby.frameworks.Arel
1818
private import codeql.ruby.frameworks.GraphQL
19+
private import codeql.ruby.frameworks.Rack
1920
private import codeql.ruby.frameworks.Rails
2021
private import codeql.ruby.frameworks.Railties
2122
private import codeql.ruby.frameworks.Stdlib

ruby/ql/lib/codeql/ruby/dataflow/internal/DataFlowPublic.qll

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1002,6 +1002,9 @@ class CallableNode extends ExprNode {
10021002
/** Gets the `n`th positional parameter. */
10031003
ParameterNode getParameter(int n) { this.getParameterPosition(result).isPositional(n) }
10041004

1005+
/** Gets the number of positional parameters of this callable. */
1006+
final int getNumberOfParameters() { result = count(this.getParameter(_)) }
1007+
10051008
/** Gets the keyword parameter of the given name. */
10061009
ParameterNode getKeywordParameter(string name) {
10071010
this.getParameterPosition(result).isKeyword(name)

ruby/ql/lib/codeql/ruby/frameworks/ActionController.qll

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -301,27 +301,39 @@ private module Request {
301301
override Http::Server::RequestInputKind getKind() { result = Http::Server::bodyInputKind() }
302302
}
303303

304-
/**
305-
* A method call on `request` which returns the rack env.
306-
* This is a hash containing all the information about the request. Values
307-
* under keys starting with `HTTP_` are user-controlled.
308-
*/
309-
private class EnvCall extends RequestMethodCall {
310-
EnvCall() { this.getMethodName() = ["env", "filtered_env"] }
311-
}
304+
private module Env {
305+
abstract private class Env extends DataFlow::LocalSourceNode { }
306+
307+
/**
308+
* A method call on `request` which returns the rack env.
309+
* This is a hash containing all the information about the request. Values
310+
* under keys starting with `HTTP_` are user-controlled.
311+
*/
312+
private class RequestEnvCall extends DataFlow::CallNode, Env {
313+
RequestEnvCall() { this.getMethodName() = ["env", "filtered_env"] }
314+
}
312315

313-
/**
314-
* A read of a user-controlled parameter from the request env.
315-
*/
316-
private class EnvHttpAccess extends DataFlow::CallNode, Http::Server::RequestInputAccess::Range {
317-
EnvHttpAccess() {
318-
this = any(EnvCall c).getAMethodCall("[]") and
319-
this.getArgument(0).getConstantValue().getString().regexpMatch("^HTTP_.+")
316+
private import codeql.ruby.frameworks.Rack
317+
318+
private class RackEnv extends Env {
319+
RackEnv() { this = any(Rack::AppCandidate app).getEnv().getALocalUse() }
320320
}
321321

322-
override Http::Server::RequestInputKind getKind() { result = Http::Server::headerInputKind() }
322+
/**
323+
* A read of a user-controlled parameter from the request env.
324+
*/
325+
private class EnvHttpAccess extends DataFlow::CallNode, Http::Server::RequestInputAccess::Range {
326+
EnvHttpAccess() {
327+
this = any(Env c).getAMethodCall("[]") and
328+
exists(string key | key = this.getArgument(0).getConstantValue().getString() |
329+
key.regexpMatch("^HTTP_.+") or key = "PATH_INFO"
330+
)
331+
}
323332

324-
override string getSourceType() { result = "ActionDispatch::Request#env[]" }
333+
override Http::Server::RequestInputKind getKind() { result = Http::Server::headerInputKind() }
334+
335+
override string getSourceType() { result = "ActionDispatch::Request#env[]" }
336+
}
325337
}
326338
}
327339

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* Provides modeling for the Rack library.
3+
*/
4+
5+
private import codeql.ruby.controlflow.CfgNodes::ExprNodes
6+
private import codeql.ruby.DataFlow
7+
private import codeql.ruby.typetracking.TypeTracker
8+
9+
/**
10+
* Provides modeling for the Rack library.
11+
*/
12+
module Rack {
13+
/**
14+
* A class that may be a rack application.
15+
* This is a class that has a `call` method that takes a single argument
16+
* (traditionally called `env`) and returns a rack-compatible response.
17+
*/
18+
class AppCandidate extends DataFlow::ClassNode {
19+
private DataFlow::MethodNode call;
20+
21+
AppCandidate() {
22+
call = this.getInstanceMethod("call") and
23+
call.getNumberOfParameters() = 1 and
24+
call.getReturn() = trackRackResponse()
25+
}
26+
27+
/**
28+
* Gets the environment of the request, which is the lone parameter to the `call` method.
29+
*/
30+
DataFlow::ParameterNode getEnv() { result = call.getParameter(0) }
31+
}
32+
33+
private predicate isRackResponse(DataFlow::Node r) {
34+
// [status, headers, body]
35+
r.asExpr().(ArrayLiteralCfgNode).getNumberOfArguments() = 3
36+
}
37+
38+
private DataFlow::LocalSourceNode trackRackResponse(TypeTracker t) {
39+
t.start() and
40+
isRackResponse(result)
41+
or
42+
exists(TypeTracker t2 | result = trackRackResponse(t2).track(t2, t))
43+
}
44+
45+
private DataFlow::Node trackRackResponse() {
46+
trackRackResponse(TypeTracker::end()).flowsTo(result)
47+
}
48+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
| rack.rb:1:1:5:3 | HelloWorld | rack.rb:2:12:2:14 | env |
2+
| rack.rb:7:1:16:3 | Proxy | rack.rb:12:12:12:18 | the_env |
3+
| rack.rb:18:1:31:3 | Logger | rack.rb:24:12:24:14 | env |
4+
| rack.rb:45:1:61:3 | Baz | rack.rb:46:12:46:14 | env |
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
private import codeql.ruby.frameworks.Rack
2+
private import codeql.ruby.DataFlow
3+
4+
query predicate rackApps(Rack::AppCandidate c, DataFlow::ParameterNode env) { env = c.getEnv() }
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
class HelloWorld
2+
def call(env)
3+
[200, {'Content-Type' => 'text/plain'}, ['Hello World']]
4+
end
5+
end
6+
7+
class Proxy
8+
def initialize(app)
9+
@app = app
10+
end
11+
12+
def call(the_env)
13+
status, headers, body = @app.call(the_env)
14+
[status, headers, body]
15+
end
16+
end
17+
18+
class Logger
19+
def initialize(app, logger = nil)
20+
@app = app
21+
@logger = logger
22+
end
23+
24+
def call(env)
25+
began_at = Utils.clock_time
26+
status, header, body = @app.call(env)
27+
header = Utils::HeaderHash.new(header)
28+
body = BodyProxy.new(body) { log(env, status, header, began_at) }
29+
[status, header, body]
30+
end
31+
end
32+
33+
class Foo
34+
def not_call(env)
35+
[1, 2, 3]
36+
end
37+
end
38+
39+
class Bar
40+
def call(env)
41+
nil
42+
end
43+
end
44+
45+
class Baz
46+
def call(env)
47+
run(env)
48+
end
49+
50+
def run(env)
51+
if env[:foo] == "foo"
52+
[200, {}, "foo"]
53+
else
54+
error
55+
end
56+
end
57+
58+
def error
59+
[400, {}, "nope"]
60+
end
61+
end

0 commit comments

Comments
 (0)