Skip to content

Commit 916b844

Browse files
authored
Merge pull request #280 from github/hmac-cli-injection
Add CLI Injection query
2 parents 36289aa + 739661e commit 916b844

File tree

14 files changed

+688
-0
lines changed

14 files changed

+688
-0
lines changed

ql/lib/codeql/ruby/Concepts.qll

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ private import codeql.ruby.CFG
99
private import codeql.ruby.DataFlow
1010
private import codeql.ruby.Frameworks
1111
private import codeql.ruby.dataflow.RemoteFlowSources
12+
private import codeql.ruby.ApiGraphs
1213

1314
/**
1415
* A data-flow node that executes SQL statements.
@@ -312,3 +313,32 @@ module HTTP {
312313
}
313314
}
314315
}
316+
317+
/**
318+
* A data flow node that executes an operating system command,
319+
* for instance by spawning a new process.
320+
*/
321+
class SystemCommandExecution extends DataFlow::Node instanceof SystemCommandExecution::Range {
322+
/** Holds if a shell interprets `arg`. */
323+
predicate isShellInterpreted(DataFlow::Node arg) { super.isShellInterpreted(arg) }
324+
325+
/** Gets an argument to this execution that specifies the command or an argument to it. */
326+
DataFlow::Node getAnArgument() { result = super.getAnArgument() }
327+
}
328+
329+
module SystemCommandExecution {
330+
/**
331+
* A data flow node that executes an operating system command, for instance by spawning a new
332+
* process.
333+
*
334+
* Extend this class to model new APIs. If you want to refine existing API models,
335+
* extend `SystemCommandExecution` instead.
336+
*/
337+
abstract class Range extends DataFlow::Node {
338+
/** Gets an argument to this execution that specifies the command or an argument to it. */
339+
abstract DataFlow::Node getAnArgument();
340+
341+
/** Holds if a shell interprets `arg`. */
342+
predicate isShellInterpreted(DataFlow::Node arg) { none() }
343+
}
344+
}

ql/lib/codeql/ruby/Frameworks.qll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55
private import codeql.ruby.frameworks.ActionController
66
private import codeql.ruby.frameworks.ActiveRecord
77
private import codeql.ruby.frameworks.ActionView
8+
private import codeql.ruby.frameworks.StandardLibrary
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
private import codeql.ruby.AST
2+
private import codeql.ruby.Concepts
3+
private import codeql.ruby.DataFlow
4+
private import codeql.ruby.ApiGraphs
5+
private import codeql.ruby.dataflow.internal.DataFlowDispatch
6+
private import codeql.ruby.dataflow.internal.DataFlowImplCommon
7+
8+
/**
9+
* The `Kernel` module is included by the `Object` class, so its methods are available
10+
* in every Ruby object. In addition, its module methods can be called by
11+
* providing a specific receiver as in `Kernel.exit`.
12+
*/
13+
class KernelMethodCall extends MethodCall {
14+
KernelMethodCall() {
15+
this = API::getTopLevelMember("Kernel").getAMethodCall(_).asExpr().getExpr()
16+
or
17+
// we assume that if there's no obvious target for this method call
18+
// and the method name matches a Kernel method, then it is a Kernel method call.
19+
// TODO: ApiGraphs should ideally handle this case
20+
not exists(DataFlowCallable method, DataFlowCall call |
21+
viableCallable(call) = method and call.getExpr() = this
22+
) and
23+
(
24+
this.getReceiver() instanceof Self and isPrivateKernelMethod(this.getMethodName())
25+
or
26+
isPublicKernelMethod(this.getMethodName())
27+
)
28+
}
29+
}
30+
31+
/**
32+
* Public methods in the `Kernel` module. These can be invoked on any object via the usual dot syntax.
33+
* ```ruby
34+
* arr = []
35+
* arr.send("push", 5) # => [5]
36+
* ```
37+
*/
38+
private predicate isPublicKernelMethod(string method) {
39+
method in ["class", "clone", "frozen?", "tap", "then", "yield_self", "send"]
40+
}
41+
42+
/**
43+
* Private methods in the `Kernel` module.
44+
* These can be be invoked on `self`, on `Kernel`, or using a low-level primitive like `send` or `instance_eval`.
45+
* ```ruby
46+
* puts "hello world"
47+
* Kernel.puts "hello world"
48+
* 5.instance_eval { puts "hello world" }
49+
* 5.send("puts", "hello world")
50+
* ```
51+
*/
52+
private predicate isPrivateKernelMethod(string method) {
53+
method in [
54+
"Array", "Complex", "Float", "Hash", "Integer", "Rational", "String", "__callee__", "__dir__",
55+
"__method__", "`", "abort", "at_exit", "autoload", "autoload?", "binding", "block_given?",
56+
"callcc", "caller", "caller_locations", "catch", "chomp", "chop", "eval", "exec", "exit",
57+
"exit!", "fail", "fork", "format", "gets", "global_variables", "gsub", "iterator?", "lambda",
58+
"load", "local_variables", "loop", "open", "p", "pp", "print", "printf", "proc", "putc",
59+
"puts", "raise", "rand", "readline", "readlines", "require", "require_relative", "select",
60+
"set_trace_func", "sleep", "spawn", "sprintf", "srand", "sub", "syscall", "system", "test",
61+
"throw", "trace_var", "trap", "untrace_var", "warn"
62+
]
63+
}
64+
65+
/**
66+
* A system command executed via subshell literal syntax.
67+
* E.g.
68+
* ```ruby
69+
* `cat foo.txt`
70+
* %x(cat foo.txt)
71+
* %x[cat foo.txt]
72+
* %x{cat foo.txt}
73+
* %x/cat foo.txt/
74+
* ```
75+
*/
76+
class SubshellLiteralExecution extends SystemCommandExecution::Range {
77+
SubshellLiteral literal;
78+
79+
SubshellLiteralExecution() { this.asExpr().getExpr() = literal }
80+
81+
override DataFlow::Node getAnArgument() { result.asExpr().getExpr() = literal.getComponent(_) }
82+
83+
override predicate isShellInterpreted(DataFlow::Node arg) { arg = getAnArgument() }
84+
}
85+
86+
/**
87+
* A system command executed via shell heredoc syntax.
88+
* E.g.
89+
* ```ruby
90+
* <<`EOF`
91+
* cat foo.text
92+
* EOF
93+
* ```
94+
*/
95+
class SubshellHeredocExecution extends SystemCommandExecution::Range {
96+
HereDoc heredoc;
97+
98+
SubshellHeredocExecution() { this.asExpr().getExpr() = heredoc and heredoc.isSubShell() }
99+
100+
override DataFlow::Node getAnArgument() { result.asExpr().getExpr() = heredoc.getComponent(_) }
101+
102+
override predicate isShellInterpreted(DataFlow::Node arg) { arg = getAnArgument() }
103+
}
104+
105+
/**
106+
* A system command executed via the `Kernel.system` method.
107+
* `Kernel.system` accepts three argument forms:
108+
* - A single string. If it contains no shell meta characters, keywords or
109+
* builtins, it is executed directly in a subprocess.
110+
* Otherwise, it is executed in a subshell.
111+
* ```ruby
112+
* system("cat foo.txt | tail")
113+
* ```
114+
* - A command and one or more arguments.
115+
* The command is executed in a subprocess.
116+
* ```ruby
117+
* system("cat", "foo.txt")
118+
* ```
119+
* - An array containing the command name and argv[0], followed by zero or more arguments.
120+
* The command is executed in a subprocess.
121+
* ```ruby
122+
* system(["cat", "cat"], "foo.txt")
123+
* ```
124+
* In addition, `Kernel.system` accepts an optional environment hash as the
125+
* first argument and an optional options hash as the last argument.
126+
* We don't yet distinguish between these arguments and the command arguments.
127+
* ```ruby
128+
* system({"FOO" => "BAR"}, "cat foo.txt | tail", {unsetenv_others: true})
129+
* ```
130+
* Ruby documentation: https://docs.ruby-lang.org/en/3.0.0/Kernel.html#method-i-system
131+
*/
132+
class KernelSystemCall extends SystemCommandExecution::Range {
133+
KernelMethodCall methodCall;
134+
135+
KernelSystemCall() {
136+
methodCall.getMethodName() = "system" and
137+
this.asExpr().getExpr() = methodCall
138+
}
139+
140+
override DataFlow::Node getAnArgument() { result.asExpr().getExpr() = methodCall.getAnArgument() }
141+
142+
override predicate isShellInterpreted(DataFlow::Node arg) {
143+
// Kernel.system invokes a subshell if you provide a single string as argument
144+
methodCall.getNumberOfArguments() = 1 and arg.asExpr().getExpr() = methodCall.getAnArgument()
145+
}
146+
}
147+
148+
/**
149+
* A system command executed via the `Kernel.exec` method.
150+
* `Kernel.exec` takes the same argument forms as `Kernel.system`. See `KernelSystemCall` for details.
151+
* Ruby documentation: https://docs.ruby-lang.org/en/3.0.0/Kernel.html#method-i-exec
152+
*/
153+
class KernelExecCall extends SystemCommandExecution::Range {
154+
KernelMethodCall methodCall;
155+
156+
KernelExecCall() {
157+
methodCall.getMethodName() = "exec" and
158+
this.asExpr().getExpr() = methodCall
159+
}
160+
161+
override DataFlow::Node getAnArgument() { result.asExpr().getExpr() = methodCall.getAnArgument() }
162+
163+
override predicate isShellInterpreted(DataFlow::Node arg) {
164+
// Kernel.exec invokes a subshell if you provide a single string as argument
165+
methodCall.getNumberOfArguments() = 1 and arg.asExpr().getExpr() = methodCall.getAnArgument()
166+
}
167+
}
168+
169+
/**
170+
* A system command executed via the `Kernel.spawn` method.
171+
* `Kernel.spawn` takes the same argument forms as `Kernel.system`.
172+
* See `KernelSystemCall` for details.
173+
* Ruby documentation: https://docs.ruby-lang.org/en/3.0.0/Kernel.html#method-i-spawn
174+
* TODO: document and handle the env and option arguments.
175+
* ```
176+
* spawn([env,] command... [,options]) → pid
177+
* ```
178+
*/
179+
class KernelSpawnCall extends SystemCommandExecution::Range {
180+
KernelMethodCall methodCall;
181+
182+
KernelSpawnCall() {
183+
methodCall.getMethodName() = "spawn" and
184+
this.asExpr().getExpr() = methodCall
185+
}
186+
187+
override DataFlow::Node getAnArgument() { result.asExpr().getExpr() = methodCall.getAnArgument() }
188+
189+
override predicate isShellInterpreted(DataFlow::Node arg) {
190+
// Kernel.spawn invokes a subshell if you provide a single string as argument
191+
methodCall.getNumberOfArguments() = 1 and arg.asExpr().getExpr() = methodCall.getAnArgument()
192+
}
193+
}
194+
195+
/**
196+
* A system command executed via one of the `Open3` methods.
197+
* These methods take the same argument forms as `Kernel.system`.
198+
* See `KernelSystemCall` for details.
199+
*/
200+
class Open3Call extends SystemCommandExecution::Range {
201+
MethodCall methodCall;
202+
203+
Open3Call() {
204+
this.asExpr().getExpr() = methodCall and
205+
this =
206+
API::getTopLevelMember("Open3")
207+
.getAMethodCall(["popen3", "popen2", "popen2e", "capture3", "capture2", "capture2e"])
208+
}
209+
210+
override DataFlow::Node getAnArgument() { result.asExpr().getExpr() = methodCall.getAnArgument() }
211+
212+
override predicate isShellInterpreted(DataFlow::Node arg) {
213+
// These Open3 methods invoke a subshell if you provide a single string as argument
214+
methodCall.getNumberOfArguments() = 1 and arg.asExpr().getExpr() = methodCall.getAnArgument()
215+
}
216+
}
217+
218+
/**
219+
* A pipeline of system commands constructed via one of the `Open3` methods.
220+
* These methods accept a variable argument list of commands.
221+
* Commands can be in any form supported by `Kernel.system`. See `KernelSystemCall` for details.
222+
* ```ruby
223+
* Open3.pipeline("cat foo.txt", "tail")
224+
* Open3.pipeline(["cat", "foo.txt"], "tail")
225+
* Open3.pipeline([{}, "cat", "foo.txt"], "tail")
226+
* Open3.pipeline([["cat", "cat"], "foo.txt"], "tail")
227+
*/
228+
class Open3PipelineCall extends SystemCommandExecution::Range {
229+
MethodCall methodCall;
230+
231+
Open3PipelineCall() {
232+
this.asExpr().getExpr() = methodCall and
233+
this =
234+
API::getTopLevelMember("Open3")
235+
.getAMethodCall(["pipeline_rw", "pipeline_r", "pipeline_w", "pipeline_start", "pipeline"])
236+
}
237+
238+
override DataFlow::Node getAnArgument() { result.asExpr().getExpr() = methodCall.getAnArgument() }
239+
240+
override predicate isShellInterpreted(DataFlow::Node arg) {
241+
// A command in the pipeline is executed in a subshell if it is given as a single string argument.
242+
arg.asExpr().getExpr() instanceof StringlikeLiteral and
243+
arg.asExpr().getExpr() = methodCall.getAnArgument()
244+
}
245+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* Provides default sources, sinks and sanitizers for reasoning about
3+
* command-injection vulnerabilities, as well as extension points for
4+
* adding your own.
5+
*/
6+
7+
private import codeql.ruby.DataFlow
8+
private import codeql.ruby.dataflow.RemoteFlowSources
9+
private import codeql.ruby.Concepts
10+
private import codeql.ruby.Frameworks
11+
private import codeql.ruby.ApiGraphs
12+
13+
module CommandInjection {
14+
/**
15+
* A data flow source for command-injection vulnerabilities.
16+
*/
17+
abstract class Source extends DataFlow::Node {
18+
/** Gets a string that describes the type of this remote flow source. */
19+
abstract string getSourceType();
20+
}
21+
22+
/**
23+
* A data flow sink for command-injection vulnerabilities.
24+
*/
25+
abstract class Sink extends DataFlow::Node { }
26+
27+
/**
28+
* A sanitizer for command-injection vulnerabilities.
29+
*/
30+
abstract class Sanitizer extends DataFlow::Node { }
31+
32+
/** A source of remote user input, considered as a flow source for command injection. */
33+
class RemoteFlowSourceAsSource extends Source {
34+
RemoteFlowSourceAsSource() { this instanceof RemoteFlowSource }
35+
36+
override string getSourceType() { result = "a user-provided value" }
37+
}
38+
39+
/**
40+
* A command argument to a function that initiates an operating system command.
41+
*/
42+
class SystemCommandExecutionSink extends Sink {
43+
SystemCommandExecutionSink() { exists(SystemCommandExecution c | c.isShellInterpreted(this)) }
44+
}
45+
46+
/**
47+
* A call to `Shellwords.escape` or `Shellwords.shellescape` sanitizes its input.
48+
*/
49+
class ShellwordsEscapeAsSanitizer extends Sanitizer {
50+
ShellwordsEscapeAsSanitizer() {
51+
this = API::getTopLevelMember("Shellwords").getAMethodCall(["escape", "shellescape"])
52+
}
53+
}
54+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* Provides a taint tracking configuration for reasoning about
3+
* command-injection vulnerabilities (CWE-078).
4+
*
5+
* Note, for performance reasons: only import this file if
6+
* `CommandInjection::Configuration` is needed, otherwise
7+
* `CommandInjectionCustomizations` should be imported instead.
8+
*/
9+
10+
import ruby
11+
import codeql.ruby.TaintTracking
12+
import CommandInjectionCustomizations::CommandInjection
13+
import codeql.ruby.DataFlow
14+
import codeql.ruby.dataflow.BarrierGuards
15+
16+
/**
17+
* A taint-tracking configuration for reasoning about command-injection vulnerabilities.
18+
*/
19+
class Configuration extends TaintTracking::Configuration {
20+
Configuration() { this = "CommandInjection" }
21+
22+
override predicate isSource(DataFlow::Node source) { source instanceof Source }
23+
24+
override predicate isSink(DataFlow::Node sink) { sink instanceof Sink }
25+
26+
override predicate isSanitizer(DataFlow::Node node) { node instanceof Sanitizer }
27+
28+
override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) {
29+
guard instanceof StringConstCompare or
30+
guard instanceof StringConstArrayInclusionCall
31+
}
32+
}

0 commit comments

Comments
 (0)