Skip to content

Commit 6f293c7

Browse files
committed
Add a query for uses of Kernel.open and IO.read
1 parent 0fcb079 commit 6f293c7

File tree

7 files changed

+162
-0
lines changed

7 files changed

+162
-0
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
<overview>
6+
<p>If <code>Kernel.open</code> is given a file name that starts with a <code>|</code>
7+
character, it will execute the remaining string as a shell command. If a
8+
malicious user can control the file name, they can execute arbitrary code.
9+
The same vulnerability applies to <code>IO.read</code>.
10+
</p>
11+
12+
</overview>
13+
<recommendation>
14+
15+
<p>Use <code>File.open</code> instead of <code>Kernel.open</code>, as the former
16+
does not have this vulnerability. Similarly, use <code>File.read</code> instead
17+
of <code>IO.read</code>.</p>
18+
19+
</recommendation>
20+
<example>
21+
22+
<p>
23+
The following example shows code that calls <code>Kernel.open</code> on a
24+
user-supplied file path.
25+
</p>
26+
27+
<sample src="examples/kernel_open.rb" />
28+
29+
<p>Instead, <code>File.open</code> should be used, as in the following example.</p>
30+
31+
<sample src="examples/file_open.rb" />
32+
33+
</example>
34+
<references>
35+
36+
<li>
37+
OWASP:
38+
<a href="https://www.owasp.org/index.php/Command_Injection">Command Injection</a>.
39+
</li>
40+
41+
<li>
42+
Example CVE: <a href="https://www.ruby-lang.org/en/news/2021/05/02/os-command-injection-in-rdoc/">Command Injection in RDoc</a>.
43+
</li>
44+
45+
</references>
46+
</qhelp>
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/**
2+
* @name Use of `Kernel.open` or `IO.read`
3+
* @description Using `Kernel.open` or `IO.read` may allow a malicious
4+
* user to execute arbitrary system commands.
5+
* @kind path-problem
6+
* @problem.severity error
7+
* @security-severity 9.8
8+
* @precision high
9+
* @id rb/kernel-open
10+
* @tags correctness
11+
* security
12+
* external/cwe/cwe-078
13+
* external/cwe/cwe-088
14+
*/
15+
16+
import ruby
17+
import codeql.ruby.ApiGraphs
18+
import codeql.ruby.frameworks.StandardLibrary
19+
import codeql.ruby.TaintTracking
20+
import codeql.ruby.dataflow.BarrierGuards
21+
import codeql.ruby.dataflow.RemoteFlowSources
22+
import DataFlow::PathGraph
23+
24+
/**
25+
* Method calls that have a suggested replacement.
26+
*/
27+
abstract class Replacement extends MethodCall {
28+
abstract string getFrom();
29+
30+
abstract string getTo();
31+
}
32+
33+
class KernelOpenCall extends KernelMethodCall, Replacement {
34+
KernelOpenCall() { this.getMethodName() = "open" }
35+
36+
override string getFrom() { result = "Kernel.open" }
37+
38+
override string getTo() { result = "File.open" }
39+
}
40+
41+
class IOReadCall extends MethodCall, Replacement {
42+
IOReadCall() { this = API::getTopLevelMember("IO").getAMethodCall("read").asExpr().getExpr() }
43+
44+
override string getFrom() { result = "IO.read" }
45+
46+
override string getTo() { result = "File.read" }
47+
}
48+
49+
class Configuration extends TaintTracking::Configuration {
50+
Configuration() { this = "KernelOpen" }
51+
52+
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
53+
54+
override predicate isSink(DataFlow::Node sink) {
55+
exists(KernelOpenCall c | c.getArgument(0) = sink.asExpr().getExpr())
56+
or
57+
exists(IOReadCall c | c.getArgument(0) = sink.asExpr().getExpr())
58+
}
59+
60+
override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) {
61+
guard instanceof StringConstCompare or
62+
guard instanceof StringConstArrayInclusionCall
63+
}
64+
}
65+
66+
from
67+
Configuration config, DataFlow::PathNode source, DataFlow::PathNode sink,
68+
DataFlow::Node sourceNode, MethodCall call
69+
where
70+
config.hasFlowPath(source, sink) and
71+
sourceNode = source.getNode() and
72+
call.getArgument(0) = sink.getNode().asExpr().getExpr()
73+
select sink.getNode(), source, sink,
74+
"This call to " + call.(Replacement).getFrom() +
75+
" depends on a user-provided value. Replace it with " + call.(Replacement).getTo() + "."
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
class UsersController < ActionController::Base
2+
def create
3+
filename = params[:filename]
4+
File.open(filename)
5+
end
6+
end
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
class UsersController < ActionController::Base
2+
def create
3+
filename = params[:filename]
4+
open(filename) # BAD
5+
end
6+
end
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
edges
2+
| KernelOpen.rb:3:12:3:17 | call to params : | KernelOpen.rb:4:10:4:13 | file |
3+
| KernelOpen.rb:3:12:3:17 | call to params : | KernelOpen.rb:5:13:5:16 | file |
4+
nodes
5+
| KernelOpen.rb:3:12:3:17 | call to params : | semmle.label | call to params : |
6+
| KernelOpen.rb:4:10:4:13 | file | semmle.label | file |
7+
| KernelOpen.rb:5:13:5:16 | file | semmle.label | file |
8+
subpaths
9+
#select
10+
| KernelOpen.rb:4:10:4:13 | file | KernelOpen.rb:3:12:3:17 | call to params : | KernelOpen.rb:4:10:4:13 | file | This call to Kernel.open depends on a user-provided value. Replace it with File.open. |
11+
| KernelOpen.rb:5:13:5:16 | file | KernelOpen.rb:3:12:3:17 | call to params : | KernelOpen.rb:5:13:5:16 | file | This call to IO.read depends on a user-provided value. Replace it with File.read. |
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
queries/security/cwe-078/KernelOpen.ql
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
class UsersController < ActionController::Base
2+
def create
3+
file = params[:file]
4+
open(file) # BAD
5+
IO.read(file) # BAD
6+
7+
File.open(file).read # GOOD
8+
9+
if file == "some/const/path.txt"
10+
open(file) # GOOD - file path is sanitised by guard
11+
end
12+
13+
if %w(some/const/1.txt some/const/2.txt).include? file
14+
IO.read(file) # GOOD - file path is sanitised by guard
15+
end
16+
end
17+
end

0 commit comments

Comments
 (0)