Skip to content

Commit 101111b

Browse files
authored
Merge pull request #9574 from hmac/hmac/action-cable-logger
Ruby: More Rails modeling
2 parents d6fd43f + e1dcc20 commit 101111b

File tree

23 files changed

+244
-23
lines changed

23 files changed

+244
-23
lines changed

ruby/ql/lib/codeql/ruby/Frameworks.qll

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*/
44

55
private import codeql.ruby.frameworks.Core
6+
private import codeql.ruby.frameworks.ActionCable
67
private import codeql.ruby.frameworks.ActionController
78
private import codeql.ruby.frameworks.ActiveRecord
89
private import codeql.ruby.frameworks.ActiveStorage
@@ -11,6 +12,7 @@ private import codeql.ruby.frameworks.ActiveSupport
1112
private import codeql.ruby.frameworks.Archive
1213
private import codeql.ruby.frameworks.GraphQL
1314
private import codeql.ruby.frameworks.Rails
15+
private import codeql.ruby.frameworks.Railties
1416
private import codeql.ruby.frameworks.Stdlib
1517
private import codeql.ruby.frameworks.Files
1618
private import codeql.ruby.frameworks.HttpClients
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* Modeling for `ActionCable`, which is a websocket gem that ships with Rails.
3+
* https://rubygems.org/gems/actioncable
4+
*/
5+
6+
private import ruby
7+
private import codeql.ruby.Concepts
8+
private import codeql.ruby.ApiGraphs
9+
private import codeql.ruby.frameworks.stdlib.Logger::Logger as StdlibLogger
10+
11+
/**
12+
* Modeling for `ActionCable`.
13+
*/
14+
module ActionCable {
15+
/**
16+
* `ActionCable::Connection::TaggedLoggerProxy`
17+
*/
18+
module Logger {
19+
private class ActionCableLoggerInstantiation extends StdlibLogger::LoggerInstantiation {
20+
ActionCableLoggerInstantiation() {
21+
this =
22+
API::getTopLevelMember("ActionCable")
23+
.getMember("Connection")
24+
.getMember("TaggedLoggerProxy")
25+
.getAnInstantiation()
26+
}
27+
}
28+
}
29+
}

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/frameworks/ActiveRecord.qll

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ private module Persistence {
359359
}
360360

361361
/**
362-
* Holds if `call` has a keyword argument of with value `value`.
362+
* Holds if `call` has a keyword argument with value `value`.
363363
*/
364364
private predicate keywordArgumentWithValue(DataFlow::CallNode call, DataFlow::ExprNode value) {
365365
exists(ExprNodes::PairCfgNode pair | pair = call.getArgument(_).asExpr() |
@@ -412,6 +412,28 @@ private module Persistence {
412412
}
413413
}
414414

415+
/**
416+
* A call to `ActiveRecord::Relation#touch_all`, which updates the `updated_at`
417+
* attribute on all records in the relation, setting it to the current time or
418+
* the time specified. If passed additional attribute names, they will also be
419+
* updated with the time.
420+
* Examples:
421+
* ```rb
422+
* Person.all.touch_all
423+
* Person.where(name: "David").touch_all
424+
* Person.all.touch_all(:created_at)
425+
* Person.all.touch_all(time: Time.new(2020, 5, 16, 0, 0, 0))
426+
* ```
427+
*/
428+
private class TouchAllCall extends DataFlow::CallNode, PersistentWriteAccess::Range {
429+
TouchAllCall() {
430+
exists(this.asExpr().getExpr().(ActiveRecordModelClassMethodCall).getReceiverClass()) and
431+
this.getMethodName() = "touch_all"
432+
}
433+
434+
override DataFlow::Node getValue() { result = this.getKeywordArgument("time") }
435+
}
436+
415437
/** A call to e.g. `User.insert_all([{name: "foo"}, {name: "bar"}])` */
416438
private class InsertAllLikeCall extends DataFlow::CallNode, PersistentWriteAccess::Range {
417439
private ExprNodes::ArrayLiteralCfgNode arr;

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ private import ruby
77
private import codeql.ruby.Concepts
88
private import codeql.ruby.DataFlow
99
private import codeql.ruby.dataflow.FlowSummary
10-
private import codeql.ruby.Concepts
1110
private import codeql.ruby.ApiGraphs
1211
private import codeql.ruby.frameworks.stdlib.Logger::Logger as StdlibLogger
1312

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/**
2+
* Modeling for `railties`, which is a gem containing various internals and utilities for the Rails framework.
3+
* https://rubygems.org/gems/railties
4+
*/
5+
6+
private import ruby
7+
private import codeql.ruby.Concepts
8+
private import codeql.ruby.ApiGraphs
9+
private import codeql.ruby.DataFlow
10+
private import codeql.ruby.ast.internal.Module
11+
12+
/**
13+
* Modeling for `railties`.
14+
*/
15+
module Railties {
16+
/**
17+
* A class which `include`s `Rails::Generators::Actions`.
18+
*/
19+
private class GeneratorsActionsContext extends ClassDeclaration {
20+
GeneratorsActionsContext() {
21+
exists(IncludeOrPrependCall i |
22+
i.getEnclosingModule() = this and
23+
i.getArgument(0) =
24+
API::getTopLevelMember("Rails")
25+
.getMember("Generators")
26+
.getMember("Actions")
27+
.getAUse()
28+
.asExpr()
29+
.getExpr()
30+
)
31+
}
32+
}
33+
34+
/**
35+
* A call to `Rails::Generators::Actions#execute_command`.
36+
* This method concatenates its first and second arguments and executes the result as a shell command.
37+
*/
38+
private class ExecuteCommandCall extends SystemCommandExecution::Range, DataFlow::CallNode {
39+
ExecuteCommandCall() {
40+
this.asExpr().getExpr().getEnclosingModule() instanceof GeneratorsActionsContext and
41+
this.getMethodName() = "execute_command"
42+
}
43+
44+
override DataFlow::Node getAnArgument() { result = this.getArgument([0, 1]) }
45+
46+
override predicate isShellInterpreted(DataFlow::Node arg) { any() }
47+
}
48+
49+
/**
50+
* A call to a method in `Rails::Generators::Actions` which delegates to `execute_command`.
51+
*/
52+
private class ExecuteCommandWrapperCall extends SystemCommandExecution::Range, DataFlow::CallNode {
53+
ExecuteCommandWrapperCall() {
54+
this.asExpr().getExpr().getEnclosingModule() instanceof GeneratorsActionsContext and
55+
this.getMethodName() = ["rake", "rails_command", "git"]
56+
}
57+
58+
override DataFlow::Node getAnArgument() { result = this.getArgument(0) }
59+
60+
override predicate isShellInterpreted(DataFlow::Node arg) { any() }
61+
}
62+
}

ruby/ql/lib/codeql/ruby/frameworks/core/internal/IOOrFile.qll

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ class IOOrFileWriteMethodCall extends IOOrFileMethodCall {
137137
receiverKind = "class" and
138138
api = ["IO", "File"] and
139139
this = API::getTopLevelMember(api).getAMethodCall(methodName) and
140-
methodName = ["binwrite", "write"] and
140+
methodName = ["binwrite", "write", "atomic_write"] and
141141
dataNode = this.getArgument(1)
142142
or
143143
// e.g. `{IO,File}.new("foo.txt", "a+).puts("hello")`

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

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

ruby/ql/test/library-tests/concepts/PersistentWriteAccess.expected

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
| app/controllers/users_controller.rb:20:7:20:57 | call to update_attributes | app/controllers/users_controller.rb:20:49:20:55 | call to get_uid |
1515
| app/controllers/users_controller.rb:23:7:23:42 | call to update_attribute | app/controllers/users_controller.rb:23:37:23:41 | "U13" |
1616
| app/controllers/users_controller.rb:26:19:26:23 | ... = ... | app/controllers/users_controller.rb:26:19:26:23 | "U14" |
17+
| app/controllers/users_controller.rb:31:7:31:32 | call to touch_all | app/controllers/users_controller.rb:31:28:31:31 | call to time |
1718
| app/models/user.rb:4:5:4:28 | call to update | app/models/user.rb:4:23:4:27 | "U15" |
1819
| app/models/user.rb:5:5:5:23 | call to update | app/models/user.rb:5:18:5:22 | "U16" |
1920
| app/models/user.rb:6:5:6:56 | call to update_attributes | app/models/user.rb:6:35:6:39 | "U17" |

ruby/ql/test/library-tests/concepts/app/controllers/users_controller.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ def create_or_modify
2525
# AssignAttributeCall
2626
user.name = "U14"
2727
user.save
28+
29+
# TouchAllCall
30+
User.touch_all
31+
User.touch_all(time: time)
2832
end
2933

3034
def get_uid

0 commit comments

Comments
 (0)