Skip to content

Commit b2958f8

Browse files
committed
ruby: rack - add redirect responses
1 parent c3ab867 commit b2958f8

File tree

3 files changed

+47
-24
lines changed

3 files changed

+47
-24
lines changed

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

Lines changed: 35 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -82,39 +82,50 @@ class MimetypeCall extends DataFlow::CallNode {
8282
string getMimeType() { mimeTypeMatches(this.getExtension(), result) }
8383
}
8484

85+
bindingset[headerName]
86+
private DataFlow::Node getHeaderValue(ResponseNode resp, string headerName) {
87+
exists(DataFlow::Node headers | headers = resp.getHeaders() |
88+
// set via `headers.<header_name>=`
89+
exists(
90+
DataFlow::CallNode contentTypeAssignment, Assignment assignment,
91+
DataFlow::PostUpdateNode postUpdateHeaders
92+
|
93+
contentTypeAssignment.getMethodName() = headerName.replaceAll("-", "_").toLowerCase() + "=" and
94+
assignment =
95+
contentTypeAssignment.getArgument(0).(DataFlow::OperationNode).asOperationAstNode() and
96+
postUpdateHeaders.(DataFlow::LocalSourceNode).flowsTo(headers) and
97+
postUpdateHeaders.getPreUpdateNode() = contentTypeAssignment.getReceiver()
98+
|
99+
result.asExpr().getExpr() = assignment.getRightOperand()
100+
)
101+
or
102+
// set within a hash
103+
exists(DataFlow::HashLiteralNode headersHash | headersHash.flowsTo(headers) |
104+
result =
105+
headersHash
106+
.getElementFromKey(any(ConstantValue v |
107+
v.getStringlikeValue().toLowerCase() = headerName.toLowerCase()
108+
))
109+
)
110+
)
111+
}
112+
85113
/** A `DataFlow::Node` returned from a rack request. */
86114
class ResponseNode extends PotentialResponseNode, Http::Server::HttpResponse::Range {
87115
ResponseNode() { this = any(AppCandidate app).getResponse() }
88116

89117
override DataFlow::Node getBody() { result = this.getElement(2) }
90118

91119
override DataFlow::Node getMimetypeOrContentTypeArg() {
92-
exists(DataFlow::Node headers | headers = this.getHeaders() |
93-
// set via `headers.content_type=`
94-
exists(
95-
DataFlow::CallNode contentTypeAssignment, Assignment assignment,
96-
DataFlow::PostUpdateNode postUpdateHeaders
97-
|
98-
contentTypeAssignment.getMethodName() = "content_type=" and
99-
assignment =
100-
contentTypeAssignment.getArgument(0).(DataFlow::OperationNode).asOperationAstNode() and
101-
postUpdateHeaders.(DataFlow::LocalSourceNode).flowsTo(headers) and
102-
postUpdateHeaders.getPreUpdateNode() = contentTypeAssignment.getReceiver()
103-
|
104-
result.asExpr().getExpr() = assignment.getRightOperand()
105-
)
106-
or
107-
// set within a hash
108-
exists(DataFlow::HashLiteralNode headersHash | headersHash.flowsTo(headers) |
109-
result =
110-
headersHash
111-
.getElementFromKey(any(ConstantValue v |
112-
v.getStringlikeValue().toLowerCase() = "content-type"
113-
))
114-
)
115-
)
120+
result = getHeaderValue(this, "content-type")
116121
}
117122

118123
// TODO
119124
override string getMimetypeDefault() { none() }
120125
}
126+
127+
class RedirectResponse extends ResponseNode, Http::Server::HttpRedirectResponse::Range {
128+
RedirectResponse() { this.getAStatusCode() = [300, 301, 302, 303, 307, 308] }
129+
130+
override DataFlow::Node getRedirectLocation() { result = getHeaderValue(this, "location") }
131+
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,7 @@ query predicate rackResponseContentTypes(Rack::ResponseNode resp, DataFlow::Node
1515
}
1616

1717
query predicate mimetypeCalls(Rack::MimetypeCall c, string mimetype) { mimetype = c.getMimeType() }
18+
19+
query predicate redirectResponses(Rack::RedirectResponse resp, DataFlow::Node location) {
20+
location = resp.getRedirectLocation()
21+
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@ def call(env)
3636
end
3737
end
3838

39+
class Redirector
40+
def call(env)
41+
status = 302
42+
headers = {'location' => '/foo.html'}
43+
[status, headers, ['this is a redirect']]
44+
end
45+
end
46+
3947
class Foo
4048
def not_call(env)
4149
[1, 2, 3]

0 commit comments

Comments
 (0)