Skip to content

Commit a5a15f3

Browse files
committed
Ruby: restructure rack model
1 parent b2958f8 commit a5a15f3

File tree

7 files changed

+160
-138
lines changed

7 files changed

+160
-138
lines changed

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

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

328328
private class RackEnv extends Env {
329-
RackEnv() { this = any(Rack::AppCandidate app).getEnv().getALocalUse() }
329+
RackEnv() { this = any(Rack::App::AppCandidate app).getEnv().getALocalUse() }
330330
}
331331

332332
/**

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,10 @@
22
* Provides modeling for the Rack library.
33
*/
44
module Rack {
5-
import rack.Rack
5+
import rack.internal.App
6+
import rack.internal.Mime
7+
import rack.internal.Response::Public as Response
8+
9+
/** DEPRECATED: Alias for App::AppCandidate */
10+
deprecated class AppCandidate = App::AppCandidate;
611
}

ruby/ql/lib/codeql/ruby/frameworks/rack/Rack.qll

Lines changed: 0 additions & 131 deletions
This file was deleted.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
private import codeql.ruby.ApiGraphs
2+
private import codeql.ruby.DataFlow
3+
private import codeql.ruby.typetracking.TypeTracker
4+
private import Response::Private as RP
5+
6+
private DataFlow::LocalSourceNode trackRackResponse(TypeTracker t, RP::PotentialResponseNode n) {
7+
t.start() and
8+
result = n
9+
or
10+
exists(TypeTracker t2 | result = trackRackResponse(t2, n).track(t2, t))
11+
}
12+
13+
private DataFlow::Node trackRackResponse(RP::PotentialResponseNode n) {
14+
trackRackResponse(TypeTracker::end(), n).flowsTo(result)
15+
}
16+
17+
module App {
18+
/**
19+
* A class that may be a rack application.
20+
* This is a class that has a `call` method that takes a single argument
21+
* (traditionally called `env`) and returns a rack-compatible response.
22+
*/
23+
class AppCandidate extends DataFlow::ClassNode {
24+
private DataFlow::MethodNode call;
25+
private RP::PotentialResponseNode resp;
26+
27+
AppCandidate() {
28+
call = this.getInstanceMethod("call") and
29+
call.getNumberOfParameters() = 1 and
30+
call.getReturn() = trackRackResponse(resp)
31+
}
32+
33+
/**
34+
* Gets the environment of the request, which is the lone parameter to the `call` method.
35+
*/
36+
DataFlow::ParameterNode getEnv() { result = call.getParameter(0) }
37+
38+
/** Gets the response returned from the request. */
39+
RP::PotentialResponseNode getResponse() { result = resp }
40+
}
41+
}

ruby/ql/lib/codeql/ruby/frameworks/rack/internal/MimeTypes.qll renamed to ruby/ql/lib/codeql/ruby/frameworks/rack/internal/Mime.qll

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
private import codeql.ruby.ApiGraphs
2+
private import codeql.ruby.DataFlow
3+
14
predicate mimeTypeMatches(string ext, string mimeType) {
25
ext = ".123" and mimeType = "application/vnd.lotus-1-2-3"
36
or
@@ -1283,3 +1286,17 @@ predicate mimeTypeMatches(string ext, string mimeType) {
12831286
or
12841287
ext = ".zmm" and mimeType = "application/vnd.handheld-entertainment+xml"
12851288
}
1289+
1290+
module Mime {
1291+
class MimetypeCall extends DataFlow::CallNode {
1292+
MimetypeCall() {
1293+
this = API::getTopLevelMember("Rack").getMember("Mime").getAMethodCall("mime_type")
1294+
}
1295+
1296+
private string getExtension() {
1297+
result = this.getArgument(0).getConstantValue().getStringlikeValue()
1298+
}
1299+
1300+
string getMimeType() { mimeTypeMatches(this.getExtension(), result) }
1301+
}
1302+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
private import codeql.ruby.AST
2+
private import codeql.ruby.ApiGraphs
3+
private import codeql.ruby.Concepts
4+
private import codeql.ruby.controlflow.CfgNodes::ExprNodes
5+
private import codeql.ruby.DataFlow
6+
private import codeql.ruby.typetracking.TypeTracker
7+
private import App as A
8+
9+
module Private {
10+
private DataFlow::LocalSourceNode trackInt(TypeTracker t, int i) {
11+
t.start() and
12+
result.getConstantValue().isInt(i)
13+
or
14+
exists(TypeTracker t2 | result = trackInt(t2, i).track(t2, t))
15+
}
16+
17+
private DataFlow::Node trackInt(int i) { trackInt(TypeTracker::end(), i).flowsTo(result) }
18+
19+
class PotentialResponseNode extends DataFlow::ArrayLiteralNode {
20+
// [status, headers, body]
21+
PotentialResponseNode() { this.getNumberOfArguments() = 3 }
22+
23+
/**
24+
* Gets an HTTP status code that may be returned in this response.
25+
*/
26+
int getAStatusCode() { this.getElement(0) = trackInt(result) }
27+
28+
/** Gets the headers returned with this response. */
29+
DataFlow::Node getHeaders() { result = this.getElement(1) }
30+
31+
/** Gets the body of this response. */
32+
DataFlow::Node getBody() { result = this.getElement(2) }
33+
}
34+
}
35+
36+
module Public {
37+
bindingset[headerName]
38+
private DataFlow::Node getHeaderValue(ResponseNode resp, string headerName) {
39+
exists(DataFlow::Node headers | headers = resp.getHeaders() |
40+
// set via `headers.<header_name>=`
41+
exists(
42+
DataFlow::CallNode contentTypeAssignment, Assignment assignment,
43+
DataFlow::PostUpdateNode postUpdateHeaders
44+
|
45+
contentTypeAssignment.getMethodName() = headerName.replaceAll("-", "_").toLowerCase() + "=" and
46+
assignment =
47+
contentTypeAssignment.getArgument(0).(DataFlow::OperationNode).asOperationAstNode() and
48+
postUpdateHeaders.(DataFlow::LocalSourceNode).flowsTo(headers) and
49+
postUpdateHeaders.getPreUpdateNode() = contentTypeAssignment.getReceiver()
50+
|
51+
result.asExpr().getExpr() = assignment.getRightOperand()
52+
)
53+
or
54+
// set within a hash
55+
exists(DataFlow::HashLiteralNode headersHash | headersHash.flowsTo(headers) |
56+
result =
57+
headersHash
58+
.getElementFromKey(any(ConstantValue v |
59+
v.getStringlikeValue().toLowerCase() = headerName.toLowerCase()
60+
))
61+
)
62+
)
63+
}
64+
65+
/** A `DataFlow::Node` returned from a rack request. */
66+
class ResponseNode extends Private::PotentialResponseNode, Http::Server::HttpResponse::Range {
67+
ResponseNode() { this = any(A::App::AppCandidate app).getResponse() }
68+
69+
override DataFlow::Node getBody() { result = this.getElement(2) }
70+
71+
override DataFlow::Node getMimetypeOrContentTypeArg() {
72+
result = getHeaderValue(this, "content-type")
73+
}
74+
75+
// TODO
76+
override string getMimetypeDefault() { none() }
77+
}
78+
79+
class RedirectResponse extends ResponseNode, Http::Server::HttpRedirectResponse::Range {
80+
RedirectResponse() { this.getAStatusCode() = [300, 301, 302, 303, 307, 308] }
81+
82+
override DataFlow::Node getRedirectLocation() { result = getHeaderValue(this, "location") }
83+
}
84+
}

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

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

5-
query predicate rackApps(Rack::AppCandidate c, DataFlow::ParameterNode env) { env = c.getEnv() }
5+
query predicate rackApps(Rack::App::AppCandidate c, DataFlow::ParameterNode env) {
6+
env = c.getEnv()
7+
}
68

7-
query predicate rackResponseStatusCodes(Rack::ResponseNode resp, string status) {
9+
query predicate rackResponseStatusCodes(Rack::Response::ResponseNode resp, string status) {
810
if exists(resp.getAStatusCode())
911
then status = resp.getAStatusCode().toString()
1012
else status = "<unknown>"
1113
}
1214

13-
query predicate rackResponseContentTypes(Rack::ResponseNode resp, DataFlow::Node contentType) {
15+
query predicate rackResponseContentTypes(
16+
Rack::Response::ResponseNode resp, DataFlow::Node contentType
17+
) {
1418
contentType = resp.getMimetypeOrContentTypeArg()
1519
}
1620

17-
query predicate mimetypeCalls(Rack::MimetypeCall c, string mimetype) { mimetype = c.getMimeType() }
21+
query predicate mimetypeCalls(Rack::Mime::MimetypeCall c, string mimetype) {
22+
mimetype = c.getMimeType()
23+
}
1824

19-
query predicate redirectResponses(Rack::RedirectResponse resp, DataFlow::Node location) {
25+
query predicate redirectResponses(Rack::Response::RedirectResponse resp, DataFlow::Node location) {
2026
location = resp.getRedirectLocation()
2127
}

0 commit comments

Comments
 (0)