Ruby: Add Module#const_get as a code execution

Module#const_get takes a single string argument and interprets it as the
name of a constant. It then looks up the constant and returns its value.

    Object.const_get("Math::PI")
    # => 3.141592653589793

By itself, this method is not as dangerous as e.g. eval, but if the
value returned is a class that is then instantiated, this can allow an
attacker to instantiate arbitrary Ruby classes.

As a result, I think it's safe to say that any remote input flowing into
this call is a potential vulnerability. A real-world example of this is
https://github.com/advisories/GHSA-52p9-v744-mwjj.
This commit is contained in:
Harry Maclean
2021-12-16 13:24:31 +13:00
parent ea538a1ee8
commit 43ddc54f2b
6 changed files with 69 additions and 0 deletions

View File

@@ -335,6 +335,18 @@ class ModuleEvalCallCodeExecution extends CodeExecution::Range, DataFlow::CallNo
override DataFlow::Node getCode() { result = this.getArgument(0) }
}
/**
* A call to `Module#const_get`, which interprets its argument as a Ruby constant.
* Passing user input to this method may result in instantiation of arbitrary Ruby classes.
*/
class ModuleConstGetCallCodeExecution extends CodeExecution::Range, DataFlow::CallNode {
ModuleConstGetCallCodeExecution() {
this.asExpr().getExpr().(UnknownMethodCall).getMethodName() = "const_get"
}
override DataFlow::Node getCode() { result = this.getArgument(0) }
}
/** Flow summary for `Regexp.escape` and its alias, `Regexp.quote`. */
class RegexpEscapeSummary extends SummarizedCallable {
RegexpEscapeSummary() { this = "Regexp.escape" }

View File

@@ -93,3 +93,10 @@ loggerLoggingCallInputs
| Logging.rb:73:5:73:63 | call to log | Logging.rb:73:36:73:45 | "message1" |
| Logging.rb:74:5:74:76 | call to log | Logging.rb:74:36:74:45 | "message2" |
| Logging.rb:74:5:74:76 | call to log | Logging.rb:74:48:74:58 | "progname2" |
moduleConstGetCallCodeExecutions
| const_get.rb:1:1:1:24 | call to const_get | const_get.rb:1:18:1:23 | "Math" |
| const_get.rb:2:1:2:28 | call to const_get | const_get.rb:2:22:2:27 | "Math" |
| const_get.rb:3:1:3:20 | call to const_get | const_get.rb:3:16:3:19 | "PI" |
| const_get.rb:4:1:4:19 | call to const_get | const_get.rb:4:16:4:18 | :PI |
| const_get.rb:22:1:22:33 | call to const_get | const_get.rb:22:18:22:32 | "Foo::Baz::VAL" |
| const_get.rb:23:1:23:25 | call to const_get | const_get.rb:23:15:23:24 | "Bar::VAL" |

View File

@@ -32,3 +32,7 @@ query DataFlow::Node moduleEvalCallCodeExecutions(ModuleEvalCallCodeExecution e)
}
query DataFlow::Node loggerLoggingCallInputs(LoggerLoggingCall c) { result = c.getAnInput() }
query DataFlow::Node moduleConstGetCallCodeExecutions(ModuleConstGetCallCodeExecution e) {
result = e.getCode()
}

View File

@@ -0,0 +1,27 @@
Object.const_get("Math")
self.class.const_get("Math")
Math.const_get("PI")
Math.const_get(:PI)
module Foo
class Bar
VAL = 10
def const_get(x)
"my custom const_get method"
end
end
class Baz < Bar
def self.const_get(x)
"another custom const_get method"
end
end
end
Object.const_get("Foo::Baz::VAL")
Foo.const_get("Bar::VAL")
# Should not be identified as a use of Module#const_get
Foo::Bar.new.const_get 5
Foo::Baz.const_get 5

View File

@@ -3,6 +3,8 @@ edges
| CodeInjection.rb:3:12:3:24 | ...[...] : | CodeInjection.rb:6:10:6:13 | code |
| CodeInjection.rb:3:12:3:24 | ...[...] : | CodeInjection.rb:18:20:18:23 | code |
| CodeInjection.rb:3:12:3:24 | ...[...] : | CodeInjection.rb:21:21:21:24 | code |
| CodeInjection.rb:3:12:3:24 | ...[...] : | CodeInjection.rb:27:15:27:18 | code |
| CodeInjection.rb:3:12:3:24 | ...[...] : | CodeInjection.rb:30:19:30:22 | code |
nodes
| CodeInjection.rb:3:12:3:17 | call to params : | semmle.label | call to params : |
| CodeInjection.rb:3:12:3:24 | ...[...] : | semmle.label | ...[...] : |
@@ -10,9 +12,13 @@ nodes
| CodeInjection.rb:9:10:9:15 | call to params | semmle.label | call to params |
| CodeInjection.rb:18:20:18:23 | code | semmle.label | code |
| CodeInjection.rb:21:21:21:24 | code | semmle.label | code |
| CodeInjection.rb:27:15:27:18 | code | semmle.label | code |
| CodeInjection.rb:30:19:30:22 | code | semmle.label | code |
subpaths
#select
| CodeInjection.rb:6:10:6:13 | code | CodeInjection.rb:3:12:3:17 | call to params : | CodeInjection.rb:6:10:6:13 | code | This code execution depends on $@. | CodeInjection.rb:3:12:3:17 | call to params | a user-provided value |
| CodeInjection.rb:9:10:9:15 | call to params | CodeInjection.rb:9:10:9:15 | call to params | CodeInjection.rb:9:10:9:15 | call to params | This code execution depends on $@. | CodeInjection.rb:9:10:9:15 | call to params | a user-provided value |
| CodeInjection.rb:18:20:18:23 | code | CodeInjection.rb:3:12:3:17 | call to params : | CodeInjection.rb:18:20:18:23 | code | This code execution depends on $@. | CodeInjection.rb:3:12:3:17 | call to params | a user-provided value |
| CodeInjection.rb:21:21:21:24 | code | CodeInjection.rb:3:12:3:17 | call to params : | CodeInjection.rb:21:21:21:24 | code | This code execution depends on $@. | CodeInjection.rb:3:12:3:17 | call to params | a user-provided value |
| CodeInjection.rb:27:15:27:18 | code | CodeInjection.rb:3:12:3:17 | call to params : | CodeInjection.rb:27:15:27:18 | code | This code execution depends on $@. | CodeInjection.rb:3:12:3:17 | call to params | a user-provided value |
| CodeInjection.rb:30:19:30:22 | code | CodeInjection.rb:3:12:3:17 | call to params : | CodeInjection.rb:30:19:30:22 | code | This code execution depends on $@. | CodeInjection.rb:3:12:3:17 | call to params | a user-provided value |

View File

@@ -22,6 +22,15 @@ class UsersController < ActionController::Base
# GOOD
Bar.class_eval(code)
# BAD
const_get(code)
# BAD
Foo.const_get(code)
# GOOD
Bar.const_get(code)
end
def update
@@ -50,4 +59,8 @@ class Bar
def self.class_eval(x)
true
end
def self.const_get(x)
true
end
end