Skip to content

Commit dd5eb98

Browse files
authored
Merge pull request github#15524 from hmac/hmac-process-spawn
Ruby: Add some more command injection sinks
2 parents 2fd2b4c + 1482411 commit dd5eb98

File tree

13 files changed

+214
-3
lines changed

13 files changed

+214
-3
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: minorAnalysis
3+
---
4+
* New command injection sinks have been added, including `Process.spawn`, `Process.exec`, `Terrapin::CommandLine` and the `open4` gem.

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55
import stdlib.Open3
66
import stdlib.Logger
77
import stdlib.Pathname
8+
import stdlib.Process

ruby/ql/lib/codeql/ruby/frameworks/stdlib/Open3.qll

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ private import codeql.ruby.ApiGraphs
77
private import codeql.ruby.Concepts
88

99
/**
10-
* Provides modeling for the `Open3` library.
10+
* Provides modeling for the `Open3` and `Open4` libraries.
1111
*/
1212
module Open3 {
1313
/**
@@ -31,6 +31,36 @@ module Open3 {
3131
}
3232
}
3333

34+
/**
35+
* A system command executed via one of the `Open4` methods.
36+
* These methods take the same argument forms as `Kernel.system`.
37+
* See `KernelSystemCall` for details.
38+
*/
39+
class Open4Call extends SystemCommandExecution::Range instanceof DataFlow::CallNode {
40+
Open4Call() {
41+
this =
42+
API::getTopLevelMember("Open4").getAMethodCall(["open4", "popen4", "spawn", "popen4ext"])
43+
}
44+
45+
override DataFlow::Node getAnArgument() {
46+
// `popen4ext` takes an optional boolean as its first argument, but it is unlikely that we will be
47+
// tracking flow into a boolean value so it doesn't seem worth modeling that special case here.
48+
result = super.getArgument(_)
49+
}
50+
51+
override predicate isShellInterpreted(DataFlow::Node arg) {
52+
super.getNumberOfArguments() = 1 and
53+
arg = this.getAnArgument()
54+
or
55+
// ```rb
56+
// Open4.popen4ext(true, "some cmd")
57+
// ```
58+
super.getNumberOfArguments() = 2 and
59+
super.getArgument(0).getConstantValue().isBoolean(_) and
60+
arg = super.getArgument(1)
61+
}
62+
}
63+
3464
/**
3565
* A pipeline of system commands constructed via one of the `Open3` methods.
3666
* These methods accept a variable argument list of commands.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* Provides modeling for the `Process` library.
3+
*/
4+
5+
private import codeql.ruby.Concepts
6+
private import codeql.ruby.DataFlow
7+
private import codeql.ruby.controlflow.CfgNodes
8+
private import codeql.ruby.frameworks.core.Kernel
9+
10+
/**
11+
* Provides modeling for the `Process` library.
12+
*/
13+
module Process {
14+
/**
15+
* A call to `Process.spawn`.
16+
* ```rb
17+
* Process.spawn("tar xf ruby-2.0.0-p195.tar.bz2")
18+
* Process.spawn({"ENV" => "VAR"}, "echo", "hi")
19+
* ```
20+
*/
21+
class SpawnCall extends SystemCommandExecution::Range instanceof DataFlow::CallNode {
22+
SpawnCall() { this = DataFlow::getConstant(["Process", "PTY"]).getAMethodCall("spawn") }
23+
24+
// The command can be argument 0 or 1
25+
// Options can be specified after the command, and we want to exclude those.
26+
override DataFlow::Node getAnArgument() {
27+
result = super.getArgument([0, 1]) and not result.asExpr() instanceof ExprNodes::PairCfgNode
28+
}
29+
30+
override predicate isShellInterpreted(DataFlow::Node arg) {
31+
// Process.spawn invokes a subshell if you provide a single string as argument
32+
super.getNumberOfArguments() = 1 and arg = this.getAnArgument()
33+
}
34+
}
35+
36+
/**
37+
* A system command executed via the `Process.exec` method.
38+
*/
39+
class ExecCall extends SystemCommandExecution::Range instanceof DataFlow::CallNode {
40+
ExecCall() { this = DataFlow::getConstant("Process").getAMethodCall("exec") }
41+
42+
override DataFlow::Node getAnArgument() { result = super.getArgument(_) }
43+
44+
override predicate isShellInterpreted(DataFlow::Node arg) {
45+
// Process.exec invokes a subshell if you provide a single string as argument
46+
super.getNumberOfArguments() = 1 and arg = this.getAnArgument()
47+
}
48+
}
49+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
extensions:
2+
- addsTo:
3+
pack: codeql/ruby-all
4+
extensible: sourceModel
5+
data: []
6+
7+
- addsTo:
8+
pack: codeql/ruby-all
9+
extensible: sinkModel
10+
data:
11+
- ["Terrapin::CommandLine!","Method[new].Argument[0]","command-injection"]
12+
- ["Terrapin::CommandLine!","Method[new].Argument[1]","command-injection"]
13+
14+
- addsTo:
15+
pack: codeql/ruby-all
16+
extensible: summaryModel
17+
data:
18+
- ["Terrapin::CommandLine::Output!","Method[new]","Argument[1]","ReturnValue","value"]
19+
- ["Terrapin::CommandLine!","Method[path=]","Argument[0]","ReturnValue","taint"]
20+
- ["Terrapin::CommandLine!","Method[new]","Argument[2]","ReturnValue","taint"]
21+
22+
- addsTo:
23+
pack: codeql/ruby-all
24+
extensible: neutralModel
25+
data: []
26+
27+
- addsTo:
28+
pack: codeql/ruby-all
29+
extensible: typeModel
30+
data:
31+
- ["Terrapin::CommandLine::Output","Terrapin::CommandLine::MultiPipe","Method[output].ReturnValue"]
32+
- ["Terrapin::CommandLine::Output","Terrapin::CommandLine::FakeRunner","Method[call].ReturnValue"]
33+
- ["Terrapin::CommandLine::Output","Terrapin::CommandLine::ProcessRunner","Method[call].ReturnValue"]
34+
- ["Terrapin::CommandLine::Output","Terrapin::CommandLine!","Method[runner].ReturnValue.ReturnValue"]
35+
- ["Terrapin::CommandLine::FakeRunner","Terrapin::CommandLine!","Method[runner].ReturnValue"]
36+
- ["Terrapin::CommandLine::Output","Terrapin::CommandLine!","Method[fake!].ReturnValue.ReturnValue"]
37+
- ["Terrapin::CommandLine::FakeRunner","Terrapin::CommandLine!","Method[fake!].ReturnValue"]
38+
- ["Terrapin::CommandLine::Output","Terrapin::CommandLine","Method[output].ReturnValue"]
39+
- ["Terrapin::CommandLineError","Terrapin::CommandNotFoundError",""]
40+
- ["Terrapin::CommandLineError","Terrapin::ExitStatusError",""]
41+
- ["Terrapin::CommandLineError","Terrapin::InterpolationError",""]
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
| Open3.rb:1:1:1:24 | call to popen3 | Open3.rb:1:14:1:23 | "echo foo" | true |
2+
| Open3.rb:2:1:2:24 | call to popen2 | Open3.rb:2:14:2:23 | "echo foo" | true |
3+
| Open3.rb:3:1:3:25 | call to popen2e | Open3.rb:3:15:3:24 | "echo foo" | true |
4+
| Open3.rb:4:1:4:26 | call to capture3 | Open3.rb:4:16:4:25 | "echo foo" | true |
5+
| Open3.rb:5:1:5:26 | call to capture2 | Open3.rb:5:16:5:25 | "echo foo" | true |
6+
| Open3.rb:6:1:6:27 | call to capture2e | Open3.rb:6:17:6:26 | "echo foo" | true |
7+
| Open3.rb:7:1:7:41 | call to pipeline_rw | Open3.rb:7:19:7:28 | "echo foo" | true |
8+
| Open3.rb:7:1:7:41 | call to pipeline_rw | Open3.rb:7:31:7:40 | "grep bar" | true |
9+
| Open3.rb:8:1:8:40 | call to pipeline_r | Open3.rb:8:18:8:27 | "echo foo" | true |
10+
| Open3.rb:8:1:8:40 | call to pipeline_r | Open3.rb:8:30:8:39 | "grep bar" | true |
11+
| Open3.rb:9:1:9:40 | call to pipeline_w | Open3.rb:9:18:9:27 | "echo foo" | true |
12+
| Open3.rb:9:1:9:40 | call to pipeline_w | Open3.rb:9:30:9:39 | "grep bar" | true |
13+
| Open3.rb:10:1:10:44 | call to pipeline_start | Open3.rb:10:22:10:31 | "echo foo" | true |
14+
| Open3.rb:10:1:10:44 | call to pipeline_start | Open3.rb:10:34:10:43 | "grep bar" | true |
15+
| Open3.rb:11:1:11:38 | call to pipeline | Open3.rb:11:16:11:25 | "echo foo" | true |
16+
| Open3.rb:11:1:11:38 | call to pipeline | Open3.rb:11:28:11:37 | "grep bar" | true |
17+
| Open3.rb:13:1:13:24 | call to open4 | Open3.rb:13:14:13:23 | "echo foo" | true |
18+
| Open3.rb:14:1:14:25 | call to popen4 | Open3.rb:14:15:14:24 | "echo foo" | true |
19+
| Open3.rb:15:1:15:23 | call to spawn | Open3.rb:15:13:15:22 | "echo bar" | true |
20+
| Open3.rb:16:1:16:27 | call to popen4ext | Open3.rb:16:17:16:26 | "echo foo" | true |
21+
| Open3.rb:17:1:17:30 | call to popen4ext | Open3.rb:17:17:17:22 | "echo" | false |
22+
| Open3.rb:17:1:17:30 | call to popen4ext | Open3.rb:17:25:17:29 | "foo" | false |
23+
| Open3.rb:18:1:18:33 | call to popen4ext | Open3.rb:18:17:18:20 | true | false |
24+
| Open3.rb:18:1:18:33 | call to popen4ext | Open3.rb:18:23:18:32 | "echo foo" | true |
25+
| Open3.rb:19:1:19:36 | call to popen4ext | Open3.rb:19:17:19:20 | true | false |
26+
| Open3.rb:19:1:19:36 | call to popen4ext | Open3.rb:19:23:19:28 | "echo" | false |
27+
| Open3.rb:19:1:19:36 | call to popen4ext | Open3.rb:19:31:19:35 | "foo" | false |
28+
| process.rb:1:1:1:25 | call to spawn | process.rb:1:15:1:24 | "echo foo" | true |
29+
| process.rb:2:1:2:30 | call to spawn | process.rb:2:15:2:29 | call to [] | true |
30+
| process.rb:3:1:3:24 | call to exec | process.rb:3:14:3:23 | "echo foo" | true |
31+
| process.rb:4:1:4:29 | call to exec | process.rb:4:14:4:28 | call to [] | true |
32+
| process.rb:5:1:5:21 | call to spawn | process.rb:5:11:5:20 | "echo foo" | true |
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import codeql.ruby.Frameworks
2+
import codeql.ruby.Concepts
3+
import codeql.ruby.DataFlow
4+
5+
query predicate commandExecutions(
6+
SystemCommandExecution execution, DataFlow::Node arg, boolean isShellInterpreted
7+
) {
8+
arg = execution.getAnArgument() and
9+
if execution.isShellInterpreted(arg)
10+
then isShellInterpreted = true
11+
else isShellInterpreted = false
12+
}

ruby/ql/test/library-tests/frameworks/stdlib/Open3.expected

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,11 @@ open3PipelineCallExecutions
1111
| Open3.rb:9:1:9:40 | call to pipeline_w |
1212
| Open3.rb:10:1:10:44 | call to pipeline_start |
1313
| Open3.rb:11:1:11:38 | call to pipeline |
14+
open4CallExecutions
15+
| Open3.rb:13:1:13:24 | call to open4 |
16+
| Open3.rb:14:1:14:25 | call to popen4 |
17+
| Open3.rb:15:1:15:23 | call to spawn |
18+
| Open3.rb:16:1:16:27 | call to popen4ext |
19+
| Open3.rb:17:1:17:30 | call to popen4ext |
20+
| Open3.rb:18:1:18:33 | call to popen4ext |
21+
| Open3.rb:19:1:19:36 | call to popen4ext |

ruby/ql/test/library-tests/frameworks/stdlib/Open3.ql

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@ import codeql.ruby.DataFlow
44
query predicate open3CallExecutions(Open3Call c) { any() }
55

66
query predicate open3PipelineCallExecutions(Open3PipelineCall c) { any() }
7+
8+
query predicate open4CallExecutions(Open4Call c) { any() }

ruby/ql/test/library-tests/frameworks/stdlib/Open3.rb

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,12 @@
88
Open3.pipeline_r("echo foo", "grep bar")
99
Open3.pipeline_w("echo foo", "grep bar")
1010
Open3.pipeline_start("echo foo", "grep bar")
11-
Open3.pipeline("echo foo", "grep bar")
11+
Open3.pipeline("echo foo", "grep bar")
12+
13+
Open4::open4("echo foo")
14+
Open4::popen4("echo foo")
15+
Open4.spawn("echo bar")
16+
Open4.popen4ext("echo foo")
17+
Open4.popen4ext("echo", "foo")
18+
Open4.popen4ext(true, "echo foo")
19+
Open4.popen4ext(true, "echo", "foo")

0 commit comments

Comments
 (0)