Compare commits

...

4 Commits

Author SHA1 Message Date
Harry Maclean
9afdab917e Ruby: Only use library inputs for libraries
Use the application heuristics to control whether we consider public
method parameters to all be sources of remote flow.
2022-03-10 16:35:13 +13:00
Harry Maclean
1915fce2d1 Ruby: Add heuristic to guess app or library
This uses the presence of various gem-related files to guess whether the
codebase is a Ruby application or a Ruby gem.
2022-03-10 16:34:01 +13:00
Harry Maclean
7a5b72b8f1 Ruby: Library input sources for Command Injection
Consider parameters of any public method to be remote flow sources for
the command injection vulnerability. This has the potential to be
noisy, but it does find several new TPs in mechanize.
2022-03-10 16:16:31 +13:00
Harry Maclean
02794d95d4 Ruby: Model Kernel.open as a command execution
If the argument to Kernel.open begins with "|", the rest of the string
is executed as a shell command.
2022-03-10 16:15:14 +13:00
6 changed files with 125 additions and 0 deletions

View File

@@ -0,0 +1,38 @@
/**
* Provides predicates to guess whether the codebase is an application or a gem.
*/
private import ruby
/**
* Provides predicates to guess whether the codebase is an application or a gem.
*/
module Application {
/**
* Holds if the codebase has a .gemspec file.
* This indicates it is a gem, or contains a gem.
*/
private predicate hasGemspec() { exists(File f | f.getExtension() = "gemspec") }
/**
* Holds if the codebase has a Gemfile.
*/
private predicate hasGemfile() { exists(File f | f.getBaseName() = "Gemfile") }
/**
* Holds if the codebase has a Gemfile.lock
* This indicates it is probably an application (though some gems erroneously have this file, too).
*/
private predicate hasGemfileLock() { exists(File f | f.getBaseName() = "Gemfile.lock") }
/**
* Holds if the codebase is likely to be a gem.
* This is a heuristic, so may be wrong.
* In particular, it will be confused by applications that contain vendored gems.
*/
predicate isGem() {
hasGemspec()
or
hasGemfile() and not hasGemfileLock()
}
}

View File

@@ -9,6 +9,8 @@ private import codeql.ruby.Concepts
private import codeql.ruby.DataFlow
private import codeql.ruby.dataflow.FlowSummary
private import codeql.ruby.dataflow.internal.DataFlowDispatch
private import codeql.ruby.controlflow.CfgNodes
private import codeql.ruby.dataflow.SSA
/** Provides modeling for the `Kernel` class. */
module Kernel {
@@ -145,6 +147,43 @@ module Kernel {
}
}
/**
* A system command executed via the `Kernel.open` method.
* If the first argument passed to `Kernel.open` starts with "|" then the rest
* of the string will be interpreted as a shell command and executed.
* Ruby documentation: https://docs.ruby-lang.org/en/3.0.0/Kernel.html#method-i-open
*/
class KernelOpenCall extends SystemCommandExecution::Range, KernelMethodCall {
KernelOpenCall() { this.getMethodName() = "open" }
override DataFlow::Node getAnArgument() { result = this.getArgument(_) }
// Kernel.open invokes a subshell if the argument starts with "|".
// If we don't know the prefix of the argument, assume it could start with "|".
override predicate isShellInterpreted(DataFlow::Node arg) {
arg = this.getAnArgument() and
not stringArgumentCannotStartWithPipe(arg)
}
predicate stringArgumentCannotStartWithPipe(DataFlow::Node arg) {
// open("prefix#{expr}")
arg.asExpr()
.(ExprNodes::StringlikeLiteralCfgNode)
.getComponent(0)
.getConstantValue()
.getString()
.charAt(0) != "|"
or
// arg = "prefix#{expr}"
// open(arg)
exists(Ssa::WriteDefinition d, ExprNodes::StringlikeLiteralCfgNode s |
d.getARead() = arg.asExpr() and d.assigns(s)
|
s.getComponent(0).getConstantValue().getString().charAt(0) != "|"
)
}
}
/**
* A call to `Kernel.eval`, which executes its first argument as Ruby code.
* ```ruby

View File

@@ -4,11 +4,13 @@
* adding your own.
*/
private import ruby
private import codeql.ruby.DataFlow
private import codeql.ruby.dataflow.RemoteFlowSources
private import codeql.ruby.Concepts
private import codeql.ruby.Frameworks
private import codeql.ruby.ApiGraphs
private import codeql.ruby.ApplicationHeuristics
module CommandInjection {
/**
@@ -36,6 +38,19 @@ module CommandInjection {
override string getSourceType() { result = "a user-provided value" }
}
/**
* Input to a public method, considered as a flow source.
* This is only correct if the codebase is a Ruby gem, rather than an application.
*/
class LibraryInput extends Source {
LibraryInput() {
Application::isGem() and
exists(Method m | not m.isPrivate() | this.asParameter() = m.getAParameter())
}
override string getSourceType() { result = "user-provided input" }
}
/**
* A command argument to a function that initiates an operating system command.
*/

View File

@@ -37,6 +37,12 @@ kernelSpawnCallExecutions
| Kernel.rb:67:1:67:58 | call to spawn |
| Kernel.rb:68:1:68:61 | call to spawn |
| Kernel.rb:69:1:69:71 | call to spawn |
kernelOpenCallExecutions
| Kernel.rb:77:1:77:21 | call to open | Kernel.rb:77:6:77:20 | "\| cat foo.txt" |
| Kernel.rb:78:1:78:26 | call to open | Kernel.rb:78:6:78:20 | "\| cat foo.txt" |
| Kernel.rb:79:1:79:51 | call to open | Kernel.rb:79:6:79:20 | "\| cat foo.txt" |
| Kernel.rb:82:1:82:9 | call to open | Kernel.rb:82:6:82:8 | cmd |
| Kernel.rb:88:3:88:17 | call to open | Kernel.rb:88:8:88:16 | maybe_cmd |
sendCallCodeExecutions
| Kernel.rb:2:1:2:22 | call to send | Kernel.rb:2:6:2:12 | "raise" |
| Kernel.rb:5:1:5:19 | call to send | Kernel.rb:5:8:5:13 | "push" |

View File

@@ -7,6 +7,10 @@ query predicate kernelExecCallExecutions(KernelExecCall c) { any() }
query predicate kernelSpawnCallExecutions(KernelSpawnCall c) { any() }
query predicate kernelOpenCallExecutions(KernelOpenCall c, DataFlow::Node arg) {
c.isShellInterpreted(arg)
}
query DataFlow::Node sendCallCodeExecutions(SendCallCodeExecution e) { result = e.getCode() }
query DataFlow::Node evalCallCodeExecutions(EvalCallCodeExecution e) { result = e.getCode() }

View File

@@ -68,6 +68,29 @@ spawn({"FOO" => "BAR"}, "echo foo", unsetenv_others: true)
spawn({"FOO" => "BAR"}, "echo", "foo", unsetenv_others: true)
spawn({"FOO" => "BAR"}, ["echo", "echo"], "foo", unsetenv_others: true)
# GOOD
open("foo.txt")
open("foo.txt", "r")
open("foo.txt", "r") { |f| f.write("hello") }
# BAD
open("| cat foo.txt")
open("| cat foo.txt", "w")
open("| cat foo.txt", "w") { |f| f.write("hello") }
cmd = "| cat foo.txt"
open(cmd) # BAD
file = "foo.txt"
open(file) # GOOD
def open_wrapped(maybe_cmd)
open(maybe_cmd) # BAD - string could start with '|'
open("foo#{maybe_cmd}.txt") # GOOD - won't cause shell execution because string doesn't start with '|'
file = "foo#{maybe_cmd}.txt"
open(file) # GOOD - same as above
end
module MockSystem
def system(*args)
args