Skip to content

Commit b346297

Browse files
committed
progress, that can flag CVE-2021-43809-vul
1 parent e971d72 commit b346297

File tree

10 files changed

+347
-2
lines changed

10 files changed

+347
-2
lines changed

ruby/ql/lib/codeql/ruby/Concepts.qll

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -713,6 +713,14 @@ class SystemCommandExecution extends DataFlow::Node instanceof SystemCommandExec
713713
/** Holds if a shell interprets `arg`. */
714714
predicate isShellInterpreted(DataFlow::Node arg) { super.isShellInterpreted(arg) }
715715

716+
/**
717+
* Gets an argument to this command execution that specifies the argument list
718+
* to the command.
719+
* TODO: Can also invlide the command.
720+
* TODO: Look through all the `SystemCommandExecution` models.
721+
*/
722+
DataFlow::Node getArgumentList() { result = super.getArgumentList() }
723+
716724
/** Gets an argument to this execution that specifies the command or an argument to it. */
717725
DataFlow::Node getAnArgument() { result = super.getAnArgument() }
718726
}
@@ -733,6 +741,12 @@ module SystemCommandExecution {
733741
/** Holds if a shell interprets `arg`. */
734742
predicate isShellInterpreted(DataFlow::Node arg) { none() }
735743

744+
/**
745+
* Gets an argument to this command execution that specifies the argument list
746+
* to the command.
747+
*/
748+
DataFlow::Node getArgumentList() { none() }
749+
736750
/** Gets an argument to this execution that specifies the command. */
737751
DataFlow::Node getACommandArgument() { none() }
738752
}

ruby/ql/lib/codeql/ruby/dataflow/internal/DataFlowDispatch.qll

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,7 @@ private module Cached {
497497
FlowSummaryImplSpecific::ParsePositions::isParsedKeywordParameterPosition(_, name)
498498
} or
499499
THashSplatArgumentPosition() or
500+
TSplatAllArgumentPosition() or
500501
TAnyArgumentPosition() or
501502
TAnyKeywordArgumentPosition()
502503

@@ -518,6 +519,7 @@ private module Cached {
518519
FlowSummaryImplSpecific::ParsePositions::isParsedKeywordArgumentPosition(_, name)
519520
} or
520521
THashSplatParameterPosition() or
522+
TSplatAllParameterPosition() or
521523
TAnyParameterPosition() or
522524
TAnyKeywordParameterPosition()
523525
}
@@ -1149,6 +1151,8 @@ class ParameterPosition extends TParameterPosition {
11491151
/** Holds if this position represents a hash-splat parameter. */
11501152
predicate isHashSplat() { this = THashSplatParameterPosition() }
11511153

1154+
predicate isSplatAll() { this = TSplatAllParameterPosition() }
1155+
11521156
/**
11531157
* Holds if this position represents any parameter, except `self` parameters. This
11541158
* includes both positional, named, and block parameters.
@@ -1172,6 +1176,8 @@ class ParameterPosition extends TParameterPosition {
11721176
or
11731177
this.isHashSplat() and result = "**"
11741178
or
1179+
this.isSplatAll() and result = "*"
1180+
or
11751181
this.isAny() and result = "any"
11761182
or
11771183
this.isAnyNamed() and result = "any-named"
@@ -1207,6 +1213,8 @@ class ArgumentPosition extends TArgumentPosition {
12071213
*/
12081214
predicate isHashSplat() { this = THashSplatArgumentPosition() }
12091215

1216+
predicate isSplatAll() { this = TSplatAllArgumentPosition() }
1217+
12101218
/** Gets a textual representation of this position. */
12111219
string toString() {
12121220
this.isSelf() and result = "self"
@@ -1222,6 +1230,8 @@ class ArgumentPosition extends TArgumentPosition {
12221230
this.isAnyNamed() and result = "any-named"
12231231
or
12241232
this.isHashSplat() and result = "**"
1233+
or
1234+
this.isSplatAll() and result = "*"
12251235
}
12261236
}
12271237

@@ -1248,6 +1258,8 @@ predicate parameterMatch(ParameterPosition ppos, ArgumentPosition apos) {
12481258
or
12491259
ppos.isHashSplat() and apos.isHashSplat()
12501260
or
1261+
ppos.isSplatAll() and apos.isSplatAll()
1262+
or
12511263
ppos.isAny() and argumentPositionIsNotSelf(apos)
12521264
or
12531265
apos.isAny() and parameterPositionIsNotSelf(ppos)

ruby/ql/lib/codeql/ruby/dataflow/internal/DataFlowPrivate.qll

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,10 @@ private class Argument extends CfgNodes::ExprCfgNode {
241241
this = call.getAnArgument() and
242242
this.getExpr() instanceof HashSplatExpr and
243243
arg.isHashSplat()
244+
or
245+
this = call.getArgument(0) and
246+
this.getExpr() instanceof SplatExpr and
247+
arg.isSplatAll()
244248
}
245249

246250
/** Holds if this expression is the `i`th argument of `c`. */
@@ -276,7 +280,8 @@ private module Cached {
276280
p instanceof SimpleParameter or
277281
p instanceof OptionalParameter or
278282
p instanceof KeywordParameter or
279-
p instanceof HashSplatParameter
283+
p instanceof HashSplatParameter or
284+
p instanceof SplatParameter
280285
} or
281286
TSelfParameterNode(MethodBase m) or
282287
TBlockParameterNode(MethodBase m) or
@@ -616,6 +621,9 @@ private module ParameterNodes {
616621
or
617622
parameter = callable.getAParameter().(HashSplatParameter) and
618623
pos.isHashSplat()
624+
or
625+
parameter = callable.getParameter(0).(SplatParameter) and
626+
pos.isSplatAll()
619627
)
620628
}
621629

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ module Open3 {
2020
Open3Call() {
2121
this =
2222
API::getTopLevelMember("Open3")
23-
.getAMethodCall(["popen3", "popen2", "popen2e", "capture3", "capture2", "capture2e"])
23+
.getAMethodCall(["popen3", "popen2", "popen2e", "capture3", "capture2", "capture2e"]) and
24+
super.getMethodName() = "capture3" and // TODO: Debugging.
25+
this.getLocation().getStartLine() = 236 // TODO: Debugging.
2426
}
2527

2628
override DataFlow::Node getAnArgument() { result = super.getArgument(_) }
@@ -34,6 +36,15 @@ module Open3 {
3436
override DataFlow::Node getACommandArgument() {
3537
result = Kernel::getACommandArgumentFromShellCall(this)
3638
}
39+
40+
override DataFlow::Node getArgumentList() {
41+
// TODO: This is incomplete
42+
exists(Cfg::CfgNodes::ExprNodes::UnaryOperationCfgNode un|
43+
un = super.getArgument(_).asExpr() and // TODO: Specific arg?
44+
un.getOperator()= "*" and
45+
result.asExpr() = un.getOperand()
46+
)
47+
}
3748
}
3849

3950
/**
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
/**
2+
* Provides default sources, sinks and sanitizers for reasoning about
3+
* second order command injection, as well as
4+
* extension points for adding your own.
5+
*/
6+
7+
private import ruby
8+
private import codeql.ruby.frameworks.core.Gem::Gem as Gem
9+
private import codeql.ruby.dataflow.RemoteFlowSources
10+
private import codeql.ruby.Concepts as Concepts
11+
12+
/** Classes and predicates for reasoning about second order command injection. */
13+
module SecondOrderCommandInjection {
14+
/** A shell command that allows for second order command injection. */
15+
private class VulnerableCommand extends string {
16+
VulnerableCommand() { this = ["git", "hg"] }
17+
18+
/**
19+
* Gets a vulnerable subcommand of this command.
20+
* E.g. `git` has `clone` and `pull` as vulnerable subcommands.
21+
* And every command of `hg` is vulnerable due to `--config=alias.<alias>=<command>`.
22+
*/
23+
bindingset[result]
24+
string getAVulnerableSubCommand() {
25+
this = "git" and result = ["clone", "ls-remote", "fetch", "pull"]
26+
or
27+
this = "hg" and exists(result)
28+
}
29+
30+
/** Gets an example argument that can cause this command to execute arbitrary code. */
31+
string getVulnerableArgumentExample() {
32+
this = "git" and result = "--upload-pack"
33+
or
34+
this = "hg" and result = "--config=alias.<alias>=<command>"
35+
}
36+
}
37+
38+
/** A source for second order command injection vulnerabilities. */
39+
abstract class Source extends DataFlow::Node {
40+
/** Gets a string that describes the source. For use in the alert message. */
41+
abstract string describe();
42+
}
43+
44+
/** A parameter of an exported function, seen as a source for second order command injection. */
45+
class ExternalInputSource extends Source {
46+
ExternalInputSource() { this = Gem::getALibraryInput() }
47+
48+
override string describe() { result = "library input" }
49+
}
50+
51+
/** A source of remote flow, seen as a source for second order command injection. */
52+
class RemoteFlowAsSource extends Source instanceof RemoteFlowSource {
53+
override string describe() { result = "a user-provided value" }
54+
}
55+
56+
/** A sink for second order command injection vulnerabilities. */
57+
abstract class Sink extends DataFlow::Node {
58+
/** Gets the command getting invoked. I.e. `git` or `hg`. */
59+
abstract string getCommand();
60+
61+
/**
62+
* Gets an example argument for the comand that allows for second order command injection.
63+
* E.g. `--upload-pack` for `git`.
64+
*/
65+
abstract string getVulnerableArgumentExample();
66+
}
67+
68+
/**
69+
* A sink that invokes a command described by the `VulnerableCommand` class.
70+
*/
71+
abstract class VulnerableCommandSink extends Sink {
72+
VulnerableCommand cmd;
73+
74+
override string getCommand() { result = cmd }
75+
76+
override string getVulnerableArgumentExample() { result = cmd.getVulnerableArgumentExample() }
77+
}
78+
79+
private import codeql.ruby.typetracking.TypeTracker
80+
81+
/** A sanitizer for second order command injection vulnerabilities. */
82+
abstract class Sanitizer extends DataFlow::Node { }
83+
84+
import codeql.ruby.typetracking.TypeTracker
85+
86+
private DataFlow::LocalSourceNode usedAsArgList(
87+
TypeBackTracker t, Concepts::SystemCommandExecution exec
88+
) {
89+
t.start() and
90+
result = exec.getArgumentList().getALocalSource()
91+
or
92+
exists(TypeBackTracker t2 |
93+
result = usedAsArgList(t2, exec).backtrack(t2, t)
94+
or
95+
// step through splat expressions
96+
t2 = t.continue() and
97+
result.asExpr().getExpr() =
98+
// TODO: flows-to.
99+
usedAsArgList(t2, exec).asExpr().getExpr().(Ast::SplatExpr).getOperand()
100+
)
101+
}
102+
103+
/** Gets a dataflow node that ends up being used as an argument list to an invocation of `git` or `hg`. */
104+
private DataFlow::LocalSourceNode usedAsVersionControlArgs(
105+
TypeBackTracker t, DataFlow::Node argList, VulnerableCommand cmd
106+
) {
107+
t.start() and
108+
// TODO: untested.
109+
exists(Concepts::SystemCommandExecution exec |
110+
exec.getACommandArgument().getConstantValue().getStringlikeValue() = cmd
111+
|
112+
exec.getArgumentList() = argList and
113+
result = argList.getALocalSource()
114+
)
115+
or
116+
t.start() and
117+
exists(
118+
Concepts::SystemCommandExecution exec, Cfg::CfgNodes::ExprNodes::ArrayLiteralCfgNode arr
119+
|
120+
argList = usedAsArgList(TypeBackTracker::end(), exec) and
121+
arr = argList.asExpr() and
122+
arr.getArgument(0).getConstantValue().getStringlikeValue() = cmd
123+
|
124+
result = argList.getALocalSource()
125+
or
126+
result
127+
.flowsTo(any(DataFlow::Node n |
128+
n.asExpr().getExpr() = arr.getArgument(_).getExpr().(Ast::SplatExpr).getOperand()
129+
))
130+
)
131+
or
132+
exists(TypeBackTracker t2 |
133+
result = usedAsVersionControlArgs(t2, argList, cmd).backtrack(t2, t)
134+
or
135+
// step through splat expressions
136+
t2 = t.continue() and
137+
result
138+
.flowsTo(any(DataFlow::Node n |
139+
n.asExpr().getExpr() =
140+
usedAsVersionControlArgs(t2, argList, cmd)
141+
.asExpr()
142+
.getExpr()
143+
.(Ast::SplatExpr)
144+
.getOperand()
145+
))
146+
)
147+
}
148+
149+
private import codeql.ruby.dataflow.internal.DataFlowDispatch as Dispatch
150+
151+
private class IndirectVcsCall extends DataFlow::CallNode {
152+
VulnerableCommand cmd;
153+
154+
IndirectVcsCall() {
155+
exists(Dispatch::DataFlowCallable calleeDis, Ast::Callable callee, DataFlow::Node list |
156+
calleeDis =
157+
Dispatch::viableCallable(any(Dispatch::DataFlowCall c | c.asCall() = this.asExpr())) and
158+
calleeDis.asCallable() = callee and
159+
list.asExpr().getExpr() = callee.getParameter(0).(Ast::SplatParameter).getDefiningAccess() and
160+
// TODO: multiple nodes in the same position, i need to figure that out if this makes production.
161+
usedAsVersionControlArgs(TypeBackTracker::end(), _, cmd).getLocation() = list.getLocation()
162+
)
163+
}
164+
165+
VulnerableCommand getCommand() { result = cmd }
166+
}
167+
168+
class IndirectCallSink extends Sink {
169+
VulnerableCommand cmd;
170+
171+
IndirectCallSink() {
172+
exists(IndirectVcsCall call, int i |
173+
call.getCommand() = cmd and
174+
call.getArgument(i).getConstantValue().getStringlikeValue() = cmd.getAVulnerableSubCommand() and
175+
this = call.getArgument(any(int j | j > i))
176+
)
177+
}
178+
179+
override string getCommand() { result = cmd }
180+
181+
override string getVulnerableArgumentExample() { result = cmd.getVulnerableArgumentExample() }
182+
}
183+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* Provides a taint tracking configuration for reasoning about
3+
* second order command-injection vulnerabilities.
4+
*
5+
* Note, for performance reasons: only import this file if
6+
* `SecondOrderCommandInjection::Configuration` is needed, otherwise
7+
* `SecondOrderCommandInjectionCustomizations` should be imported instead.
8+
*/
9+
10+
import ruby
11+
import SecondOrderCommandInjectionCustomizations::SecondOrderCommandInjection
12+
import codeql.ruby.TaintTracking
13+
14+
/**
15+
* A taint-tracking configuration for reasoning about second order command-injection vulnerabilities.
16+
*/
17+
class Configuration extends TaintTracking::Configuration {
18+
Configuration() { this = "SecondOrderCommandInjection" }
19+
20+
override predicate isSource(DataFlow::Node source) { source instanceof Source }
21+
22+
override predicate isSink(DataFlow::Node sink) { sink instanceof Sink }
23+
24+
override predicate isSanitizer(DataFlow::Node node) { node instanceof Sanitizer }
25+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy