Skip to content

Commit c50a6c1

Browse files
authored
Merge pull request #318 from github/hmac-open-query
Add a query for uses of `Kernel.open` and `IO.read`
2 parents 1d12159 + 7bf818f commit c50a6c1

File tree

10 files changed

+211
-70
lines changed

10 files changed

+211
-70
lines changed

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

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,10 @@ private DataFlow::Node fileInstanceInstantiation() {
3636
result = API::getTopLevelMember("File").getAMethodCall("open")
3737
or
3838
// Calls to `Kernel.open` can yield `File` instances
39-
exists(KernelMethodCall c |
40-
c = result.asExpr().getExpr() and
41-
c.getMethodName() = "open" and
42-
// Assume that calls that don't invoke shell commands will instead open
43-
// a file.
44-
not pathArgSpawnsSubprocess(c.getArgument(0))
45-
)
39+
result.(KernelMethodCall).getMethodName() = "open" and
40+
// Assume that calls that don't invoke shell commands will instead open
41+
// a file.
42+
not pathArgSpawnsSubprocess(result.(KernelMethodCall).getArgument(0).asExpr().getExpr())
4643
}
4744

4845
private DataFlow::Node fileInstance() {

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

Lines changed: 43 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,27 @@ private import codeql.ruby.ApiGraphs
88
* in every Ruby object. In addition, its module methods can be called by
99
* providing a specific receiver as in `Kernel.exit`.
1010
*/
11-
class KernelMethodCall extends MethodCall {
11+
class KernelMethodCall extends DataFlow::CallNode {
12+
private MethodCall methodCall;
13+
1214
KernelMethodCall() {
13-
this = API::getTopLevelMember("Kernel").getAMethodCall(_).asExpr().getExpr()
14-
or
15-
this instanceof UnknownMethodCall and
15+
methodCall = this.asExpr().getExpr() and
1616
(
17-
this.getReceiver() instanceof Self and isPrivateKernelMethod(this.getMethodName())
17+
this = API::getTopLevelMember("Kernel").getAMethodCall(_)
1818
or
19-
isPublicKernelMethod(this.getMethodName())
19+
methodCall instanceof UnknownMethodCall and
20+
(
21+
this.getReceiver().asExpr().getExpr() instanceof Self and
22+
isPrivateKernelMethod(methodCall.getMethodName())
23+
or
24+
isPublicKernelMethod(methodCall.getMethodName())
25+
)
2026
)
2127
}
28+
29+
string getMethodName() { result = methodCall.getMethodName() }
30+
31+
int getNumberOfArguments() { result = methodCall.getNumberOfArguments() }
2232
}
2333

2434
/**
@@ -161,19 +171,14 @@ class SubshellHeredocExecution extends SystemCommandExecution::Range {
161171
* ```
162172
* Ruby documentation: https://docs.ruby-lang.org/en/3.0.0/Kernel.html#method-i-system
163173
*/
164-
class KernelSystemCall extends SystemCommandExecution::Range {
165-
KernelMethodCall methodCall;
174+
class KernelSystemCall extends SystemCommandExecution::Range, KernelMethodCall {
175+
KernelSystemCall() { this.getMethodName() = "system" }
166176

167-
KernelSystemCall() {
168-
methodCall.getMethodName() = "system" and
169-
this.asExpr().getExpr() = methodCall
170-
}
171-
172-
override DataFlow::Node getAnArgument() { result.asExpr().getExpr() = methodCall.getAnArgument() }
177+
override DataFlow::Node getAnArgument() { result = this.getArgument(_) }
173178

174179
override predicate isShellInterpreted(DataFlow::Node arg) {
175180
// Kernel.system invokes a subshell if you provide a single string as argument
176-
methodCall.getNumberOfArguments() = 1 and arg.asExpr().getExpr() = methodCall.getAnArgument()
181+
this.getNumberOfArguments() = 1 and arg = getAnArgument()
177182
}
178183
}
179184

@@ -182,19 +187,14 @@ class KernelSystemCall extends SystemCommandExecution::Range {
182187
* `Kernel.exec` takes the same argument forms as `Kernel.system`. See `KernelSystemCall` for details.
183188
* Ruby documentation: https://docs.ruby-lang.org/en/3.0.0/Kernel.html#method-i-exec
184189
*/
185-
class KernelExecCall extends SystemCommandExecution::Range {
186-
KernelMethodCall methodCall;
190+
class KernelExecCall extends SystemCommandExecution::Range, KernelMethodCall {
191+
KernelExecCall() { this.getMethodName() = "exec" }
187192

188-
KernelExecCall() {
189-
methodCall.getMethodName() = "exec" and
190-
this.asExpr().getExpr() = methodCall
191-
}
192-
193-
override DataFlow::Node getAnArgument() { result.asExpr().getExpr() = methodCall.getAnArgument() }
193+
override DataFlow::Node getAnArgument() { result = this.getArgument(_) }
194194

195195
override predicate isShellInterpreted(DataFlow::Node arg) {
196196
// Kernel.exec invokes a subshell if you provide a single string as argument
197-
methodCall.getNumberOfArguments() = 1 and arg.asExpr().getExpr() = methodCall.getAnArgument()
197+
this.getNumberOfArguments() = 1 and arg = getAnArgument()
198198
}
199199
}
200200

@@ -208,19 +208,14 @@ class KernelExecCall extends SystemCommandExecution::Range {
208208
* spawn([env,] command... [,options]) -> pid
209209
* ```
210210
*/
211-
class KernelSpawnCall extends SystemCommandExecution::Range {
212-
KernelMethodCall methodCall;
211+
class KernelSpawnCall extends SystemCommandExecution::Range, KernelMethodCall {
212+
KernelSpawnCall() { this.getMethodName() = "spawn" }
213213

214-
KernelSpawnCall() {
215-
methodCall.getMethodName() = "spawn" and
216-
this.asExpr().getExpr() = methodCall
217-
}
218-
219-
override DataFlow::Node getAnArgument() { result.asExpr().getExpr() = methodCall.getAnArgument() }
214+
override DataFlow::Node getAnArgument() { result = this.getArgument(_) }
220215

221216
override predicate isShellInterpreted(DataFlow::Node arg) {
222217
// Kernel.spawn invokes a subshell if you provide a single string as argument
223-
methodCall.getNumberOfArguments() = 1 and arg.asExpr().getExpr() = methodCall.getAnArgument()
218+
this.getNumberOfArguments() = 1 and arg = getAnArgument()
224219
}
225220
}
226221

@@ -284,14 +279,10 @@ class Open3PipelineCall extends SystemCommandExecution::Range {
284279
* a # => 2
285280
* ```
286281
*/
287-
class EvalCallCodeExecution extends CodeExecution::Range {
288-
KernelMethodCall methodCall;
289-
290-
EvalCallCodeExecution() {
291-
this.asExpr().getExpr() = methodCall and methodCall.getMethodName() = "eval"
292-
}
282+
class EvalCallCodeExecution extends CodeExecution::Range, KernelMethodCall {
283+
EvalCallCodeExecution() { this.getMethodName() = "eval" }
293284

294-
override DataFlow::Node getCode() { result.asExpr().getExpr() = methodCall.getArgument(0) }
285+
override DataFlow::Node getCode() { result = this.getArgument(0) }
295286
}
296287

297288
/**
@@ -302,51 +293,41 @@ class EvalCallCodeExecution extends CodeExecution::Range {
302293
* arr # => [1]
303294
* ```
304295
*/
305-
class SendCallCodeExecution extends CodeExecution::Range {
306-
KernelMethodCall methodCall;
296+
class SendCallCodeExecution extends CodeExecution::Range, KernelMethodCall {
297+
SendCallCodeExecution() { this.getMethodName() = "send" }
307298

308-
SendCallCodeExecution() {
309-
this.asExpr().getExpr() = methodCall and methodCall.getMethodName() = "send"
310-
}
311-
312-
override DataFlow::Node getCode() { result.asExpr().getExpr() = methodCall.getArgument(0) }
299+
override DataFlow::Node getCode() { result = this.getArgument(0) }
313300
}
314301

315302
/**
316303
* A call to `BasicObject#instance_eval`, which executes its first argument as Ruby code.
317304
*/
318-
class InstanceEvalCallCodeExecution extends CodeExecution::Range {
319-
BasicObjectInstanceMethodCall methodCall;
320-
305+
class InstanceEvalCallCodeExecution extends CodeExecution::Range, DataFlow::CallNode {
321306
InstanceEvalCallCodeExecution() {
322-
this.asExpr().getExpr() = methodCall and methodCall.getMethodName() = "instance_eval"
307+
this.asExpr().getExpr().(UnknownMethodCall).getMethodName() = "instance_eval"
323308
}
324309

325-
override DataFlow::Node getCode() { result.asExpr().getExpr() = methodCall.getArgument(0) }
310+
override DataFlow::Node getCode() { result = this.getArgument(0) }
326311
}
327312

328313
/**
329314
* A call to `Module#class_eval`, which executes its first argument as Ruby code.
330315
*/
331-
class ClassEvalCallCodeExecution extends CodeExecution::Range {
332-
UnknownMethodCall methodCall;
333-
316+
class ClassEvalCallCodeExecution extends CodeExecution::Range, DataFlow::CallNode {
334317
ClassEvalCallCodeExecution() {
335-
this.asExpr().getExpr() = methodCall and methodCall.getMethodName() = "class_eval"
318+
this.asExpr().getExpr().(UnknownMethodCall).getMethodName() = "class_eval"
336319
}
337320

338-
override DataFlow::Node getCode() { result.asExpr().getExpr() = methodCall.getArgument(0) }
321+
override DataFlow::Node getCode() { result = this.getArgument(0) }
339322
}
340323

341324
/**
342325
* A call to `Module#module_eval`, which executes its first argument as Ruby code.
343326
*/
344-
class ModuleEvalCallCodeExecution extends CodeExecution::Range {
345-
UnknownMethodCall methodCall;
346-
327+
class ModuleEvalCallCodeExecution extends CodeExecution::Range, DataFlow::CallNode {
347328
ModuleEvalCallCodeExecution() {
348-
this.asExpr().getExpr() = methodCall and methodCall.getMethodName() = "module_eval"
329+
this.asExpr().getExpr().(UnknownMethodCall).getMethodName() = "module_eval"
349330
}
350331

351-
override DataFlow::Node getCode() { result.asExpr().getExpr() = methodCall.getArgument(0) }
332+
override DataFlow::Node getCode() { result = this.getArgument(0) }
352333
}

ql/lib/codeql/ruby/frameworks/http_clients/OpenURI.qll

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class OpenURIRequest extends HTTP::Client::Request::Range {
2626
or
2727
// Kernel.open("http://example.com").read
2828
// open("http://example.com").read
29-
this instanceof KernelMethodCall and
29+
request instanceof KernelMethodCall and
3030
this.getMethodName() = "open" and
3131
request.asExpr().getExpr() = this and
3232
responseBody.asExpr().getExpr().(MethodCall).getMethodName() in ["read", "readlines"] and
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: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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+
* external/cwe/cwe-073
15+
*/
16+
17+
import ruby
18+
import codeql.ruby.ApiGraphs
19+
import codeql.ruby.frameworks.StandardLibrary
20+
import codeql.ruby.TaintTracking
21+
import codeql.ruby.dataflow.BarrierGuards
22+
import codeql.ruby.dataflow.RemoteFlowSources
23+
import DataFlow::PathGraph
24+
25+
/**
26+
* Method calls that have a suggested replacement.
27+
*/
28+
abstract class Replacement extends DataFlow::CallNode {
29+
abstract string getFrom();
30+
31+
abstract string getTo();
32+
}
33+
34+
class KernelOpenCall extends KernelMethodCall, Replacement {
35+
KernelOpenCall() { this.getMethodName() = "open" }
36+
37+
override string getFrom() { result = "Kernel.open" }
38+
39+
override string getTo() { result = "File.open" }
40+
}
41+
42+
class IOReadCall extends DataFlow::CallNode, Replacement {
43+
IOReadCall() { this = API::getTopLevelMember("IO").getAMethodCall("read") }
44+
45+
override string getFrom() { result = "IO.read" }
46+
47+
override string getTo() { result = "File.read" }
48+
}
49+
50+
class Configuration extends TaintTracking::Configuration {
51+
Configuration() { this = "KernelOpen" }
52+
53+
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
54+
55+
override predicate isSink(DataFlow::Node sink) {
56+
exists(KernelOpenCall c | c.getArgument(0) = sink)
57+
or
58+
exists(IOReadCall c | c.getArgument(0) = sink)
59+
}
60+
61+
override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) {
62+
guard instanceof StringConstCompare or
63+
guard instanceof StringConstArrayInclusionCall
64+
}
65+
}
66+
67+
from
68+
Configuration config, DataFlow::PathNode source, DataFlow::PathNode sink,
69+
DataFlow::Node sourceNode, DataFlow::CallNode call
70+
where
71+
config.hasFlowPath(source, sink) and
72+
sourceNode = source.getNode() and
73+
call.asExpr().getExpr().(MethodCall).getArgument(0) = sink.getNode().asExpr().getExpr()
74+
select sink.getNode(), source, sink,
75+
"This call to " + call.(Replacement).getFrom() +
76+
" 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)