Skip to content

Commit ab1f341

Browse files
authored
Merge pull request github#13566 from alexrford/rb/rack-params
Ruby: add `Rack::Request` params and cookies as remote input sources
2 parents 11f2681 + 2b0b285 commit ab1f341

File tree

8 files changed

+106
-0
lines changed

8 files changed

+106
-0
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+
* Query parameters and cookies from `Rack::Response` objects are recognized as potential sources of remote flow input.
5+
* Calls to `Rack::Utils.parse_query` now propagate taint.

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
*/
88
module Rack {
99
import rack.internal.App
10+
import rack.internal.Request
1011
import rack.internal.Response::Public as Response
12+
import rack.internal.Utils
1113

1214
/** DEPRECATED: Alias for App::AppCandidate */
1315
deprecated class AppCandidate = App::AppCandidate;
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* Provides modeling for the `Request` component of the `Rack` library.
3+
*/
4+
5+
private import codeql.ruby.AST
6+
private import codeql.ruby.ApiGraphs
7+
private import codeql.ruby.Concepts
8+
private import codeql.ruby.DataFlow
9+
10+
/**
11+
* Provides modeling for the `Request` component of the `Rack` library.
12+
*/
13+
module Request {
14+
private class RackRequest extends API::Node {
15+
RackRequest() { this = API::getTopLevelMember("Rack").getMember("Request").getInstance() }
16+
}
17+
18+
/** An access to the parameters of a request to a rack application via a `Rack::Request` instance. */
19+
private class RackRequestParamsAccess extends Http::Server::RequestInputAccess::Range {
20+
RackRequestParamsAccess() {
21+
this = any(RackRequest req).getAMethodCall(["params", "query_string", "[]", "fullpath"])
22+
}
23+
24+
override string getSourceType() { result = "Rack::Request#params" }
25+
26+
override Http::Server::RequestInputKind getKind() {
27+
result = Http::Server::parameterInputKind()
28+
}
29+
}
30+
31+
/** An access to the cookies of a request to a rack application via a `Rack::Request` instance. */
32+
private class RackRequestCookiesAccess extends Http::Server::RequestInputAccess::Range {
33+
RackRequestCookiesAccess() { this = any(RackRequest req).getAMethodCall("cookies") }
34+
35+
override string getSourceType() { result = "Rack::Request#cookies" }
36+
37+
override Http::Server::RequestInputKind getKind() { result = Http::Server::cookieInputKind() }
38+
}
39+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* Provides modeling for the `Utils` component of the `Rack` library.
3+
*/
4+
5+
private import codeql.ruby.ApiGraphs
6+
private import codeql.ruby.dataflow.FlowSummary
7+
8+
/**
9+
* Provides modeling for the `Utils` component of the `Rack` library.
10+
*/
11+
module Utils {
12+
/** Flow summary for `Rack::Utils.parse_query`, which parses a query string. */
13+
private class ParseQuerySummary extends SummarizedCallable {
14+
ParseQuerySummary() { this = "Rack::Utils.parse_query" }
15+
16+
override MethodCall getACall() {
17+
result =
18+
API::getTopLevelMember("Rack")
19+
.getMember("Utils")
20+
.getAMethodCall("parse_query")
21+
.asExpr()
22+
.getExpr()
23+
}
24+
25+
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
26+
input = "Argument[0]" and output = "ReturnValue" and preservesValue = false
27+
}
28+
}
29+
}

ruby/ql/test/library-tests/dataflow/local/TaintStep.expected

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2816,6 +2816,7 @@
28162816
| file://:0:0:0:0 | [summary param] position 0 in Mysql2::Client.escape() | file://:0:0:0:0 | [summary] to write: ReturnValue in Mysql2::Client.escape() |
28172817
| file://:0:0:0:0 | [summary param] position 0 in Mysql2::Client.new() | file://:0:0:0:0 | [summary] to write: ReturnValue in Mysql2::Client.new() |
28182818
| file://:0:0:0:0 | [summary param] position 0 in PG.new() | file://:0:0:0:0 | [summary] to write: ReturnValue in PG.new() |
2819+
| file://:0:0:0:0 | [summary param] position 0 in Rack::Utils.parse_query | file://:0:0:0:0 | [summary] to write: ReturnValue in Rack::Utils.parse_query |
28192820
| file://:0:0:0:0 | [summary param] position 0 in SQLite3::Database.quote() | file://:0:0:0:0 | [summary] to write: ReturnValue in SQLite3::Database.quote() |
28202821
| file://:0:0:0:0 | [summary param] position 0 in Sequel.connect | file://:0:0:0:0 | [summary] to write: ReturnValue in Sequel.connect |
28212822
| file://:0:0:0:0 | [summary param] position 0 in String.try_convert | file://:0:0:0:0 | [summary] to write: ReturnValue in String.try_convert |

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ rackRequestHandlers
66
| rack.rb:60:3:62:5 | call | rack.rb:60:12:60:14 | env | rack.rb:66:7:66:22 | call to [] |
77
| rack.rb:60:3:62:5 | call | rack.rb:60:12:60:14 | env | rack.rb:73:5:73:21 | call to [] |
88
| 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.rb:98:3:107:5 | call | rack.rb:98:12:98:14 | env | rack.rb:110:5:110:28 | call to [] |
10+
| rack.rb:98:3:107:5 | call | rack.rb:98:12:98:14 | env | rack.rb:114:5:114:30 | call to [] |
911
| 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 [] |
1012
| 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 [] |
1113
| rack_apps.rb:21:14:21:50 | -> { ... } | rack_apps.rb:21:17:21:19 | env | rack_apps.rb:21:24:21:48 | call to [] |
@@ -16,3 +18,7 @@ rackResponseContentTypes
1618
redirectResponses
1719
| rack.rb:43:5:43:45 | call to [] | rack.rb:42:30:42:40 | "/foo.html" |
1820
| rack.rb:93:5:93:78 | call to finish | rack.rb:93:60:93:70 | redirect_to |
21+
requestInputAccesses
22+
| rack.rb:100:18:100:28 | call to cookies |
23+
| rack.rb:103:14:103:23 | call to params |
24+
| rack.rb:104:18:104:32 | ...[...] |

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
private import codeql.ruby.AST
2+
private import codeql.ruby.Concepts
23
private import codeql.ruby.frameworks.Rack
34
private import codeql.ruby.DataFlow
45

@@ -17,3 +18,5 @@ query predicate rackResponseContentTypes(
1718
query predicate redirectResponses(Rack::Response::RedirectResponse resp, DataFlow::Node location) {
1819
location = resp.getRedirectLocation()
1920
}
21+
22+
query predicate requestInputAccesses(Http::Server::RequestInputAccess ria) { any() }

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,24 @@ def do_redirect
9393
Rack::Response.new(['redirecting'], 302, 'Location' => redirect_to).finish
9494
end
9595
end
96+
97+
class UsesRequest
98+
def call(env)
99+
req = Rack::Request.new(env)
100+
if session = req.cookies['session']
101+
reuse_session(session)
102+
else
103+
name = req.params['name']
104+
password = req['password']
105+
login(name, password)
106+
end
107+
end
108+
109+
def login(name, password)
110+
[200, {}, "new session"]
111+
end
112+
113+
def reuse_session(name, password)
114+
[200, {}, "reuse session"]
115+
end
116+
end

0 commit comments

Comments
 (0)