Skip to content

Commit 7dfab37

Browse files
committed
Ruby: Model redirect_back and redirect_back_or_to
These are ActionController methods that redirect to the HTTP Referer, falling back to the given location if there is no Referer.
1 parent a298f5e commit 7dfab37

File tree

7 files changed

+84
-20
lines changed

7 files changed

+84
-20
lines changed

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

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -180,18 +180,31 @@ private class ActionControllerHtmlEscapeCall extends HtmlEscapeCall {
180180
* specific URL/path or to a different action in this controller.
181181
*/
182182
class RedirectToCall extends ActionControllerContextCall {
183-
RedirectToCall() { this.getMethodName() = "redirect_to" }
183+
RedirectToCall() {
184+
this.getMethodName() = ["redirect_to", "redirect_back", "redirect_back_or_to"]
185+
}
184186

185187
/** Gets the `Expr` representing the URL to redirect to, if any */
186-
Expr getRedirectUrl() { result = this.getArgument(0) }
188+
Expr getRedirectUrl() {
189+
this.getMethodName() = "redirect_back" and result = this.getKeywordArgument("fallback_location")
190+
or
191+
this.getMethodName() = ["redirect_to", "redirect_back_or_to"] and result = this.getArgument(0)
192+
}
187193

188194
/** Gets the `ActionControllerActionMethod` to redirect to, if any */
189195
ActionControllerActionMethod getRedirectActionMethod() {
190-
exists(string methodName |
191-
this.getKeywordArgument("action").getConstantValue().isStringlikeValue(methodName) and
192-
methodName = result.getName() and
193-
result.getEnclosingModule() = this.getControllerClass()
194-
)
196+
this.getKeywordArgument("action").getConstantValue().isStringlikeValue(result.getName()) and
197+
result.getEnclosingModule() = this.getControllerClass()
198+
}
199+
200+
/**
201+
* Holds if this method call allows a redirect to an external host.
202+
*/
203+
predicate allowsExternalRedirect() {
204+
// Unless the option allow_other_host is explicitly set to false, assume that external redirects are allowed.
205+
// TODO: Take into account `config.action_controller.raise_on_open_redirects`.
206+
// TODO: Take into account that this option defaults to false in Rails 7.
207+
not this.getKeywordArgument("allow_other_host").getConstantValue().isBoolean(false)
195208
}
196209
}
197210

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,9 @@ module UrlRedirect {
6969
// We exclude any handlers with names containing create/update/destroy, as these are not likely to handle GET requests.
7070
not exists(method.(ActionControllerActionMethod).getARoute()) and
7171
not method.getName().regexpMatch(".*(create|update|destroy).*")
72-
)
72+
) and
73+
// If this redirect is an ActionController method call, it is only vulnerable if it allows external redirects.
74+
forall(RedirectToCall c | c = e.asExpr().getExpr() | c.allowsExternalRedirect())
7375
)
7476
}
7577
}

ruby/ql/test/library-tests/frameworks/ActionController.expected

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ actionControllerControllerClasses
33
| ActiveRecord.rb:41:1:64:3 | BarController |
44
| ActiveRecord.rb:66:1:70:3 | BazController |
55
| app/controllers/comments_controller.rb:1:1:7:3 | CommentsController |
6-
| app/controllers/foo/bars_controller.rb:3:1:31:3 | BarsController |
6+
| app/controllers/foo/bars_controller.rb:3:1:39:3 | BarsController |
77
| app/controllers/photos_controller.rb:1:1:4:3 | PhotosController |
88
| app/controllers/posts_controller.rb:1:1:10:3 | PostsController |
99
| app/controllers/users/notifications_controller.rb:2:3:5:5 | NotificationsController |
@@ -17,6 +17,8 @@ actionControllerActionMethods
1717
| app/controllers/foo/bars_controller.rb:5:3:7:5 | index |
1818
| app/controllers/foo/bars_controller.rb:9:3:18:5 | show_debug |
1919
| app/controllers/foo/bars_controller.rb:20:3:24:5 | show |
20+
| app/controllers/foo/bars_controller.rb:26:3:28:5 | go_back |
21+
| app/controllers/foo/bars_controller.rb:30:3:32:5 | go_back_2 |
2022
| app/controllers/photos_controller.rb:2:3:3:5 | show |
2123
| app/controllers/posts_controller.rb:2:3:3:5 | index |
2224
| app/controllers/posts_controller.rb:5:3:6:5 | show |
@@ -66,10 +68,12 @@ cookiesSources
6668
| app/controllers/foo/bars_controller.rb:10:27:10:33 | call to cookies |
6769
redirectToCalls
6870
| app/controllers/foo/bars_controller.rb:17:5:17:30 | call to redirect_to |
71+
| app/controllers/foo/bars_controller.rb:27:5:27:39 | call to redirect_back_or_to |
72+
| app/controllers/foo/bars_controller.rb:31:5:31:56 | call to redirect_back |
6973
actionControllerHelperMethods
7074
getAssociatedControllerClasses
71-
| app/controllers/foo/bars_controller.rb:3:1:31:3 | BarsController | app/views/foo/bars/_widget.html.erb:0:0:0:0 | app/views/foo/bars/_widget.html.erb |
72-
| app/controllers/foo/bars_controller.rb:3:1:31:3 | BarsController | app/views/foo/bars/show.html.erb:0:0:0:0 | app/views/foo/bars/show.html.erb |
75+
| app/controllers/foo/bars_controller.rb:3:1:39:3 | BarsController | app/views/foo/bars/_widget.html.erb:0:0:0:0 | app/views/foo/bars/_widget.html.erb |
76+
| app/controllers/foo/bars_controller.rb:3:1:39:3 | BarsController | app/views/foo/bars/show.html.erb:0:0:0:0 | app/views/foo/bars/show.html.erb |
7377
controllerTemplateFiles
74-
| app/controllers/foo/bars_controller.rb:3:1:31:3 | BarsController | app/views/foo/bars/_widget.html.erb:0:0:0:0 | app/views/foo/bars/_widget.html.erb |
75-
| app/controllers/foo/bars_controller.rb:3:1:31:3 | BarsController | app/views/foo/bars/show.html.erb:0:0:0:0 | app/views/foo/bars/show.html.erb |
78+
| app/controllers/foo/bars_controller.rb:3:1:39:3 | BarsController | app/views/foo/bars/_widget.html.erb:0:0:0:0 | app/views/foo/bars/_widget.html.erb |
79+
| app/controllers/foo/bars_controller.rb:3:1:39:3 | BarsController | app/views/foo/bars/show.html.erb:0:0:0:0 | app/views/foo/bars/show.html.erb |

ruby/ql/test/library-tests/frameworks/ActionView.expected

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ rawCalls
1414
renderCalls
1515
| app/controllers/foo/bars_controller.rb:6:5:6:37 | call to render |
1616
| app/controllers/foo/bars_controller.rb:23:5:23:76 | call to render |
17-
| app/controllers/foo/bars_controller.rb:29:5:29:17 | call to render |
17+
| app/controllers/foo/bars_controller.rb:37:5:37:17 | call to render |
1818
| app/views/foo/bars/show.html.erb:31:5:31:89 | call to render |
1919
renderToCalls
2020
| app/controllers/foo/bars_controller.rb:15:16:15:97 | call to render_to_string |

ruby/ql/test/library-tests/frameworks/app/controllers/foo/bars_controller.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@ def show
2323
render "foo/bars/show", locals: { display_text: dt, safe_text: "hello" }
2424
end
2525

26+
def go_back
27+
redirect_back_or_to action: "index"
28+
end
29+
30+
def go_back_2
31+
redirect_back fallback_location: { action: "index" }
32+
end
33+
2634
private
2735

2836
def unreachable_action

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

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@ 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:63:21:63:32 | input_params : |
6+
| UrlRedirect.rb:24:31:24:36 | call to params : | UrlRedirect.rb:88:21:88: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" |
99
| 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 : |
10+
| UrlRedirect.rb:63:38:63:43 | call to params : | UrlRedirect.rb:63:38:63:49 | ...[...] |
11+
| UrlRedirect.rb:68:38:68:43 | call to params : | UrlRedirect.rb:68:38:68:49 | ...[...] |
12+
| UrlRedirect.rb:73:25:73:30 | call to params : | UrlRedirect.rb:73:25:73:36 | ...[...] |
13+
| UrlRedirect.rb:88:21:88:32 | input_params : | UrlRedirect.rb:89:5:89:29 | call to permit : |
1114
nodes
1215
| UrlRedirect.rb:4:17:4:22 | call to params | semmle.label | call to params |
1316
| UrlRedirect.rb:9:17:9:22 | call to params : | semmle.label | call to params : |
@@ -23,10 +26,16 @@ nodes
2326
| UrlRedirect.rb:34:20:34:31 | ...[...] : | semmle.label | ...[...] : |
2427
| UrlRedirect.rb:58:17:58:22 | call to params : | semmle.label | call to params : |
2528
| 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 : |
29+
| UrlRedirect.rb:63:38:63:43 | call to params : | semmle.label | call to params : |
30+
| UrlRedirect.rb:63:38:63:49 | ...[...] | semmle.label | ...[...] |
31+
| UrlRedirect.rb:68:38:68:43 | call to params : | semmle.label | call to params : |
32+
| UrlRedirect.rb:68:38:68:49 | ...[...] | semmle.label | ...[...] |
33+
| UrlRedirect.rb:73:25:73:30 | call to params : | semmle.label | call to params : |
34+
| UrlRedirect.rb:73:25:73:36 | ...[...] | semmle.label | ...[...] |
35+
| UrlRedirect.rb:88:21:88:32 | input_params : | semmle.label | input_params : |
36+
| UrlRedirect.rb:89:5:89:29 | call to permit : | semmle.label | call to permit : |
2837
subpaths
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 |
38+
| UrlRedirect.rb:24:31:24:36 | call to params : | UrlRedirect.rb:88:21:88:32 | input_params : | UrlRedirect.rb:89:5:89:29 | call to permit : | UrlRedirect.rb:24:17:24:37 | call to filter_params |
3039
#select
3140
| 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 |
3241
| 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 |
@@ -35,3 +44,6 @@ subpaths
3544
| 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 |
3645
| 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 |
3746
| 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 |
47+
| UrlRedirect.rb:63:38:63:49 | ...[...] | UrlRedirect.rb:63:38:63:43 | call to params : | UrlRedirect.rb:63:38:63:49 | ...[...] | Untrusted URL redirection due to $@. | UrlRedirect.rb:63:38:63:43 | call to params | a user-provided value |
48+
| UrlRedirect.rb:68:38:68:49 | ...[...] | UrlRedirect.rb:68:38:68:43 | call to params : | UrlRedirect.rb:68:38:68:49 | ...[...] | Untrusted URL redirection due to $@. | UrlRedirect.rb:68:38:68:43 | call to params | a user-provided value |
49+
| UrlRedirect.rb:73:25:73:36 | ...[...] | UrlRedirect.rb:73:25:73:30 | call to params : | UrlRedirect.rb:73:25:73:36 | ...[...] | Untrusted URL redirection due to $@. | UrlRedirect.rb:73:25:73:30 | call to params | a user-provided value |

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

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,36 @@ def create1
5353

5454
# BAD
5555
# The same as `create1` but this is reachable via a GET request, as configured
56-
# by the routes at the top of this file.
56+
# by the routes at the bottom of this file.
5757
def route9
5858
redirect_to params[:key]
5959
end
6060

61+
# BAD
62+
def route10
63+
redirect_back fallback_location: params[:key]
64+
end
65+
66+
# BAD
67+
def route11
68+
redirect_back fallback_location: params[:key], allow_other_host: true
69+
end
70+
71+
# BAD
72+
def route12
73+
redirect_back_or_to params[:key]
74+
end
75+
76+
# GOOD
77+
def route13
78+
redirect_back fallback_location: params[:key], allow_other_host: false
79+
end
80+
81+
# GOOD
82+
def route14
83+
redirect_back_or_to params[:key], allow_other_host: false
84+
end
85+
6186
private
6287

6388
def filter_params(input_params)

0 commit comments

Comments
 (0)