Skip to content

Commit 314683d

Browse files
committed
Ruby: Improve UrlRedirect query using Rails routes
Handlers for non-GET requests aren't vulnerable to URL redirect attacks, because browsers won't initiate non-GET requests when you click a link. We can use Rails routing information, if present, to filter out any handlers for non-GET requests.
1 parent 751d8a7 commit 314683d

File tree

3 files changed

+41
-15
lines changed

3 files changed

+41
-15
lines changed

ruby/ql/lib/codeql/ruby/security/UrlRedirectCustomizations.qll

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ private import codeql.ruby.Concepts
1010
private import codeql.ruby.dataflow.RemoteFlowSources
1111
private import codeql.ruby.dataflow.BarrierGuards
1212
private import codeql.ruby.dataflow.Sanitizers
13+
private import codeql.ruby.frameworks.ActionController
1314

1415
/**
1516
* Provides default sources, sinks and sanitizers for detecting
@@ -54,15 +55,25 @@ module UrlRedirect {
5455
*/
5556
class RedirectLocationAsSink extends Sink {
5657
RedirectLocationAsSink() {
57-
exists(HTTP::Server::HttpRedirectResponse e |
58+
exists(HTTP::Server::HttpRedirectResponse e, MethodBase method |
5859
this = e.getRedirectLocation() and
59-
// As a rough heuristic, assume that methods with these names are handlers for POST/PUT/PATCH/DELETE requests,
60-
// which are not as vulnerable to URL redirection because browsers will not initiate them from clicking a link.
61-
not this.asExpr()
62-
.getExpr()
63-
.getEnclosingMethod()
64-
.getName()
65-
.regexpMatch(".*(create|update|destroy).*")
60+
// We only want handlers for GET requests.
61+
// Handlers for other HTTP methods are not as vulnerable to URL
62+
// redirection as browsers will not initiate them from clicking a link.
63+
method = this.asExpr().getExpr().getEnclosingMethod() and
64+
(
65+
// If there's a Rails GET route to this handler, we can be certain that it is a candiate.
66+
method.(ActionControllerActionMethod).getARoute().getHTTPMethod() = "get"
67+
or
68+
// Otherwise, we have to rely on a heuristic to filter out invulnerable handlers.
69+
// We exclude any handlers with names containing create/update/destroy, as these are not likely to handle GET requests.
70+
not exists(method.(ActionControllerActionMethod).getARoute()) and
71+
not this.asExpr()
72+
.getExpr()
73+
.getEnclosingMethod()
74+
.getName()
75+
.regexpMatch(".*(create|update|destroy).*")
76+
)
6677
)
6778
}
6879
}

ruby/ql/test/query-tests/security/cwe-601/UrlRedirect.expected

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ edges
33
| UrlRedirect.rb:14:17:14:22 | call to params : | UrlRedirect.rb:14:17:14:43 | call to fetch |
44
| UrlRedirect.rb:19:17:19:22 | call to params : | UrlRedirect.rb:19:17:19:37 | call to to_unsafe_hash |
55
| UrlRedirect.rb:24:31:24:36 | call to params : | UrlRedirect.rb:24:17:24:37 | call to filter_params |
6-
| UrlRedirect.rb:24:31:24:36 | call to params : | UrlRedirect.rb:56:21:56:32 | input_params : |
6+
| UrlRedirect.rb:24:31:24:36 | call to params : | UrlRedirect.rb:63:21:63:32 | input_params : |
77
| UrlRedirect.rb:34:20:34:25 | call to params : | UrlRedirect.rb:34:20:34:31 | ...[...] : |
88
| UrlRedirect.rb:34:20:34:31 | ...[...] : | UrlRedirect.rb:34:17:34:37 | "#{...}/foo" |
9-
| UrlRedirect.rb:56:21:56:32 | input_params : | UrlRedirect.rb:57:5:57:29 | call to permit : |
9+
| UrlRedirect.rb:58:17:58:22 | call to params : | UrlRedirect.rb:58:17:58:28 | ...[...] |
10+
| UrlRedirect.rb:63:21:63:32 | input_params : | UrlRedirect.rb:64:5:64:29 | call to permit : |
1011
nodes
1112
| UrlRedirect.rb:4:17:4:22 | call to params | semmle.label | call to params |
1213
| UrlRedirect.rb:9:17:9:22 | call to params : | semmle.label | call to params : |
@@ -20,14 +21,17 @@ nodes
2021
| UrlRedirect.rb:34:17:34:37 | "#{...}/foo" | semmle.label | "#{...}/foo" |
2122
| UrlRedirect.rb:34:20:34:25 | call to params : | semmle.label | call to params : |
2223
| UrlRedirect.rb:34:20:34:31 | ...[...] : | semmle.label | ...[...] : |
23-
| UrlRedirect.rb:56:21:56:32 | input_params : | semmle.label | input_params : |
24-
| UrlRedirect.rb:57:5:57:29 | call to permit : | semmle.label | call to permit : |
24+
| UrlRedirect.rb:58:17:58:22 | call to params : | semmle.label | call to params : |
25+
| UrlRedirect.rb:58:17:58:28 | ...[...] | semmle.label | ...[...] |
26+
| UrlRedirect.rb:63:21:63:32 | input_params : | semmle.label | input_params : |
27+
| UrlRedirect.rb:64:5:64:29 | call to permit : | semmle.label | call to permit : |
2528
subpaths
26-
| UrlRedirect.rb:24:31:24:36 | call to params : | UrlRedirect.rb:56:21:56:32 | input_params : | UrlRedirect.rb:57:5:57:29 | call to permit : | UrlRedirect.rb:24:17:24:37 | call to filter_params |
29+
| UrlRedirect.rb:24:31:24:36 | call to params : | UrlRedirect.rb:63:21:63:32 | input_params : | UrlRedirect.rb:64:5:64:29 | call to permit : | UrlRedirect.rb:24:17:24:37 | call to filter_params |
2730
#select
2831
| UrlRedirect.rb:4:17:4:22 | call to params | UrlRedirect.rb:4:17:4:22 | call to params | UrlRedirect.rb:4:17:4:22 | call to params | Untrusted URL redirection due to $@. | UrlRedirect.rb:4:17:4:22 | call to params | a user-provided value |
2932
| UrlRedirect.rb:9:17:9:28 | ...[...] | UrlRedirect.rb:9:17:9:22 | call to params : | UrlRedirect.rb:9:17:9:28 | ...[...] | Untrusted URL redirection due to $@. | UrlRedirect.rb:9:17:9:22 | call to params | a user-provided value |
3033
| UrlRedirect.rb:14:17:14:43 | call to fetch | UrlRedirect.rb:14:17:14:22 | call to params : | UrlRedirect.rb:14:17:14:43 | call to fetch | Untrusted URL redirection due to $@. | UrlRedirect.rb:14:17:14:22 | call to params | a user-provided value |
3134
| UrlRedirect.rb:19:17:19:37 | call to to_unsafe_hash | UrlRedirect.rb:19:17:19:22 | call to params : | UrlRedirect.rb:19:17:19:37 | call to to_unsafe_hash | Untrusted URL redirection due to $@. | UrlRedirect.rb:19:17:19:22 | call to params | a user-provided value |
3235
| UrlRedirect.rb:24:17:24:37 | call to filter_params | UrlRedirect.rb:24:31:24:36 | call to params : | UrlRedirect.rb:24:17:24:37 | call to filter_params | Untrusted URL redirection due to $@. | UrlRedirect.rb:24:31:24:36 | call to params | a user-provided value |
3336
| UrlRedirect.rb:34:17:34:37 | "#{...}/foo" | UrlRedirect.rb:34:20:34:25 | call to params : | UrlRedirect.rb:34:17:34:37 | "#{...}/foo" | Untrusted URL redirection due to $@. | UrlRedirect.rb:34:20:34:25 | call to params | a user-provided value |
37+
| UrlRedirect.rb:58:17:58:28 | ...[...] | UrlRedirect.rb:58:17:58:22 | call to params : | UrlRedirect.rb:58:17:58:28 | ...[...] | Untrusted URL redirection due to $@. | UrlRedirect.rb:58:17:58:22 | call to params | a user-provided value |

ruby/ql/test/query-tests/security/cwe-601/UrlRedirect.rb

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,16 @@ def route8
4545
end
4646

4747
# GOOD
48-
# Technically vulnerable, but we assume this is a handler for a POST request,
48+
# Technically vulnerable, this is a handler for a POST request,
4949
# so can't be triggered by following a link.
50-
def create
50+
def create1
51+
redirect_to params[:key]
52+
end
53+
54+
# BAD
55+
# The same as `create1` but this is reachable via a GET request, as configured
56+
# by the routes at the top of this file.
57+
def route9
5158
redirect_to params[:key]
5259
end
5360

@@ -57,3 +64,7 @@ def filter_params(input_params)
5764
input_params.permit(:key)
5865
end
5966
end
67+
68+
Rails.routes.draw do
69+
get "/r9", to: "users#route9"
70+
end

0 commit comments

Comments
 (0)