Skip to content

Commit b6912de

Browse files
authored
Merge pull request github#13483 from alexrford/rb/rack-extend-app-and-resp
Ruby: rack - model more responses and app types
2 parents 9eae946 + 5fafd9e commit b6912de

File tree

8 files changed

+158
-25
lines changed

8 files changed

+158
-25
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
category: minorAnalysis
3+
---
4+
* More kinds of rack applications are now recognized.
5+
* Rack::Response instances are now recognized as potential responses from rack applications.

ruby/ql/lib/codeql/ruby/frameworks/actiondispatch/internal/Request.qll

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ module Request {
128128
private import codeql.ruby.frameworks.Rack
129129

130130
private class RackEnv extends Env {
131-
RackEnv() { this = any(Rack::App::AppCandidate app).getEnv().getALocalUse() }
131+
RackEnv() { this = any(Rack::App::RequestHandler handler).getEnv().getALocalUse() }
132132
}
133133

134134
/**

ruby/ql/lib/codeql/ruby/frameworks/rack/internal/App.qll

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,44 @@
22
* Provides modeling for Rack applications.
33
*/
44

5+
private import codeql.ruby.AST
56
private import codeql.ruby.ApiGraphs
67
private import codeql.ruby.DataFlow
78
private import codeql.ruby.typetracking.TypeTracker
89
private import Response::Private as RP
910

10-
/** A method node for a method named `call`. */
11-
private class CallMethodNode extends DataFlow::MethodNode {
12-
CallMethodNode() { this.getMethodName() = "call" }
11+
/**
12+
* A callable node that takes a single argument and, if it has a method name,
13+
* is called "call".
14+
*/
15+
private class PotentialRequestHandler extends DataFlow::CallableNode {
16+
PotentialRequestHandler() {
17+
this.getNumberOfParameters() = 1 and
18+
(
19+
this.(DataFlow::MethodNode).getMethodName() = "call"
20+
or
21+
not this instanceof DataFlow::MethodNode and
22+
exists(DataFlow::CallNode cn | cn.getMethodName() = "run" |
23+
this.(DataFlow::LocalSourceNode).flowsTo(cn.getArgument(0))
24+
or
25+
// TODO: `Proc.new` should automatically propagate flow from its block argument
26+
any(DataFlow::CallNode proc |
27+
proc = API::getTopLevelMember("Proc").getAnInstantiation() and
28+
proc.getBlock() = this
29+
).(DataFlow::LocalSourceNode).flowsTo(cn.getArgument(0))
30+
)
31+
)
32+
}
1333
}
1434

15-
private DataFlow::LocalSourceNode trackRackResponse(TypeBackTracker t, CallMethodNode call) {
35+
private DataFlow::LocalSourceNode trackRackResponse(TypeBackTracker t, PotentialRequestHandler call) {
1636
t.start() and
1737
result = call.getAReturnNode().getALocalSource()
1838
or
1939
exists(TypeBackTracker t2 | result = trackRackResponse(t2, call).backtrack(t2, t))
2040
}
2141

22-
private RP::PotentialResponseNode trackRackResponse(CallMethodNode call) {
42+
private RP::PotentialResponseNode trackRackResponse(PotentialRequestHandler call) {
2343
result = trackRackResponse(TypeBackTracker::end(), call)
2444
}
2545

@@ -28,12 +48,13 @@ private RP::PotentialResponseNode trackRackResponse(CallMethodNode call) {
2848
*/
2949
module App {
3050
/**
51+
* DEPRECATED: Use `RequestHandler` instead.
3152
* A class that may be a rack application.
3253
* This is a class that has a `call` method that takes a single argument
3354
* (traditionally called `env`) and returns a rack-compatible response.
3455
*/
35-
class AppCandidate extends DataFlow::ClassNode {
36-
private CallMethodNode call;
56+
deprecated class AppCandidate extends DataFlow::ClassNode {
57+
private RequestHandler call;
3758
private RP::PotentialResponseNode resp;
3859

3960
AppCandidate() {
@@ -50,4 +71,19 @@ module App {
5071
/** Gets the response returned from a request to this application. */
5172
RP::PotentialResponseNode getResponse() { result = resp }
5273
}
74+
75+
/**
76+
* A callable node that looks like it implements the rack specification.
77+
*/
78+
class RequestHandler extends PotentialRequestHandler {
79+
private RP::PotentialResponseNode resp;
80+
81+
RequestHandler() { resp = trackRackResponse(this) }
82+
83+
/** Gets the `env` parameter passed to this request handler. */
84+
DataFlow::ParameterNode getEnv() { result = this.getParameter(0) }
85+
86+
/** Gets a response returned from this request handler. */
87+
RP::PotentialResponseNode getAResponse() { result = resp }
88+
}
5389
}

ruby/ql/lib/codeql/ruby/frameworks/rack/internal/Response.qll

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,39 @@ private import App as A
1313
/** Contains implementation details for modeling `Rack::Response`. */
1414
module Private {
1515
/** A `DataFlow::Node` that may be a rack response. This is detected heuristically, if something "looks like" a rack response syntactically then we consider it to be a potential response node. */
16-
class PotentialResponseNode extends DataFlow::ArrayLiteralNode {
17-
// [status, headers, body]
18-
PotentialResponseNode() { this.getNumberOfArguments() = 3 }
19-
16+
abstract class PotentialResponseNode extends DataFlow::Node {
2017
/** Gets the headers returned with this response. */
21-
DataFlow::Node getHeaders() { result = this.getElement(1) }
18+
abstract DataFlow::Node getHeaders();
2219

2320
/** Gets the body of this response. */
24-
DataFlow::Node getBody() { result = this.getElement(2) }
21+
abstract DataFlow::Node getBody();
22+
}
23+
24+
/** A rack response constructed directly using an array literal. */
25+
private class PotentialArrayResponse extends PotentialResponseNode, DataFlow::ArrayLiteralNode {
26+
// [status, headers, body]
27+
PotentialArrayResponse() { this.getNumberOfArguments() = 3 }
28+
29+
override DataFlow::Node getHeaders() { result = this.getElement(1) }
30+
31+
override DataFlow::Node getBody() { result = this.getElement(2) }
32+
}
33+
34+
/** A rack response constructed by calling `finish` on an instance of `Rack::Response`. */
35+
private class RackResponseConstruction extends PotentialResponseNode, DataFlow::CallNode {
36+
private DataFlow::CallNode responseConstruction;
37+
38+
// (body, status, headers)
39+
RackResponseConstruction() {
40+
responseConstruction =
41+
API::getTopLevelMember("Rack").getMember("Response").getAnInstantiation() and
42+
this = responseConstruction.getAMethodCall() and
43+
this.getMethodName() = "finish"
44+
}
45+
46+
override DataFlow::Node getHeaders() { result = responseConstruction.getArgument(2) }
47+
48+
override DataFlow::Node getBody() { result = responseConstruction.getArgument(0) }
2549
}
2650
}
2751

@@ -54,19 +78,30 @@ module Public {
5478
v.getStringlikeValue().toLowerCase() = headerName.toLowerCase()
5579
))
5680
)
81+
or
82+
// pair in a `Rack::Response.new` constructor
83+
exists(DataFlow::PairNode headerPair | headerPair = headers |
84+
headerPair.getKey().getConstantValue().getStringlikeValue().toLowerCase() =
85+
headerName.toLowerCase() and
86+
result = headerPair.getValue()
87+
)
5788
)
5889
}
5990

6091
/** A `DataFlow::Node` returned from a rack request. */
61-
class ResponseNode extends Private::PotentialResponseNode, Http::Server::HttpResponse::Range {
62-
ResponseNode() { this = any(A::App::AppCandidate app).getResponse() }
92+
class ResponseNode extends Http::Server::HttpResponse::Range instanceof Private::PotentialResponseNode
93+
{
94+
ResponseNode() { this = any(A::App::RequestHandler handler).getAResponse() }
6395

64-
override DataFlow::Node getBody() { result = this.getElement(2) }
96+
override DataFlow::Node getBody() { result = this.(Private::PotentialResponseNode).getBody() }
6597

6698
override DataFlow::Node getMimetypeOrContentTypeArg() {
6799
result = getHeaderValue(this, "content-type")
68100
}
69101

102+
/** Gets the headers returned with this response. */
103+
DataFlow::Node getHeaders() { result = this.(Private::PotentialResponseNode).getHeaders() }
104+
70105
// TODO: is there a sensible value for this?
71106
override string getMimetypeDefault() { none() }
72107
}
Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
1-
rackApps
2-
| rack.rb:1:1:10:3 | HelloWorld | rack.rb:2:12:2:14 | env |
3-
| rack.rb:12:1:22:3 | Proxy | rack.rb:17:12:17:18 | the_env |
4-
| rack.rb:24:1:37:3 | Logger | rack.rb:30:12:30:14 | env |
5-
| rack.rb:39:1:45:3 | Redirector | rack.rb:40:12:40:14 | env |
6-
| rack.rb:59:1:75:3 | Baz | rack.rb:60:12:60:14 | env |
1+
rackRequestHandlers
2+
| rack.rb:2:3:9:5 | call | rack.rb:2:12:2:14 | env | rack.rb:8:5:8:38 | call to [] |
3+
| rack.rb:17:3:21:5 | call | rack.rb:17:12:17:18 | the_env | rack.rb:20:5:20:27 | call to [] |
4+
| rack.rb:30:3:36:5 | call | rack.rb:30:12:30:14 | env | rack.rb:35:5:35:26 | call to [] |
5+
| rack.rb:40:3:44:5 | call | rack.rb:40:12:40:14 | env | rack.rb:43:5:43:45 | call to [] |
6+
| rack.rb:60:3:62:5 | call | rack.rb:60:12:60:14 | env | rack.rb:66:7:66:22 | call to [] |
7+
| rack.rb:60:3:62:5 | call | rack.rb:60:12:60:14 | env | rack.rb:73:5:73:21 | call to [] |
8+
| rack.rb:79:3:81:5 | call | rack.rb:79:17:79:19 | env | rack.rb:93:5:93:78 | call to finish |
9+
| rack_apps.rb:6:3:12:5 | call | rack_apps.rb:6:12:6:14 | env | rack_apps.rb:10:12:10:34 | call to [] |
10+
| rack_apps.rb:16:3:18:5 | call | rack_apps.rb:16:17:16:19 | env | rack_apps.rb:17:5:17:28 | call to [] |
11+
| rack_apps.rb:21:14:21:50 | -> { ... } | rack_apps.rb:21:17:21:19 | env | rack_apps.rb:21:24:21:48 | call to [] |
12+
| rack_apps.rb:23:21:23:53 | { ... } | rack_apps.rb:23:24:23:26 | env | rack_apps.rb:23:29:23:51 | call to [] |
713
rackResponseContentTypes
814
| rack.rb:8:5:8:38 | call to [] | rack.rb:7:34:7:45 | "text/plain" |
915
| rack.rb:20:5:20:27 | call to [] | rack.rb:19:28:19:38 | "text/html" |
1016
redirectResponses
1117
| rack.rb:43:5:43:45 | call to [] | rack.rb:42:30:42:40 | "/foo.html" |
18+
| rack.rb:93:5:93:78 | call to finish | rack.rb:93:60:93:70 | redirect_to |

ruby/ql/test/library-tests/frameworks/rack/Rack.ql

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ private import codeql.ruby.AST
22
private import codeql.ruby.frameworks.Rack
33
private import codeql.ruby.DataFlow
44

5-
query predicate rackApps(Rack::App::AppCandidate c, DataFlow::ParameterNode env) {
6-
env = c.getEnv()
5+
query predicate rackRequestHandlers(
6+
Rack::App::RequestHandler handler, DataFlow::ParameterNode env, Rack::Response::ResponseNode resp
7+
) {
8+
env = handler.getEnv() and resp = handler.getAResponse()
79
}
810

911
query predicate rackResponseContentTypes(

ruby/ql/test/library-tests/frameworks/rack/rack.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,23 @@ def error
7373
[400, {}, "nope"]
7474
end
7575
end
76+
77+
class Qux
78+
attr_reader :env
79+
def self.call(env)
80+
new(env).call
81+
end
82+
83+
def initialize(env)
84+
@env = env
85+
end
86+
87+
def call
88+
do_redirect
89+
end
90+
91+
def do_redirect
92+
redirect_to = env['redirect_to']
93+
Rack::Response.new(['redirecting'], 302, 'Location' => redirect_to).finish
94+
end
95+
end
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
require 'rack'
2+
require 'rack/handler/puma'
3+
handler = Rack::Handler::Puma
4+
5+
class InstanceApp
6+
def call(env)
7+
status = 200
8+
headers = {}
9+
body = ["instance app"]
10+
resp = [status, headers, body]
11+
resp
12+
end
13+
end
14+
15+
class ClassApp
16+
def self.call(env)
17+
[200, {}, ["class app"]]
18+
end
19+
end
20+
21+
lambda_app = ->(env) { [200, {}, ["lambda app"]] }
22+
23+
proc_app = Proc.new { |env| [200, {}, ["proc app"]] }
24+
25+
handler.run InstanceApp.new
26+
handler.run ClassApp
27+
handler.run lambda_app
28+
handler.run proc_app

0 commit comments

Comments
 (0)