Skip to content

Commit 187cfd7

Browse files
committed
add isShellInterpreted to the SystemCommandExecution concept
1 parent b8bd98e commit 187cfd7

File tree

4 files changed

+50
-4
lines changed

4 files changed

+50
-4
lines changed

python/ql/lib/semmle/python/Concepts.qll

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ private import semmle.python.security.internal.EncryptionKeySizes
1919
* extend `SystemCommandExecution::Range` instead.
2020
*/
2121
class SystemCommandExecution extends DataFlow::Node instanceof SystemCommandExecution::Range {
22+
/** Holds if a shell interprets `arg`. */
23+
predicate isShellInterpreted(DataFlow::Node arg) { super.isShellInterpreted(arg) }
24+
2225
/** Gets the argument that specifies the command to be executed. */
2326
DataFlow::Node getCommand() { result = super.getCommand() }
2427
}
@@ -35,6 +38,9 @@ module SystemCommandExecution {
3538
abstract class Range extends DataFlow::Node {
3639
/** Gets the argument that specifies the command to be executed. */
3740
abstract DataFlow::Node getCommand();
41+
42+
/** Holds if a shell interprets `arg`. */
43+
predicate isShellInterpreted(DataFlow::Node arg) { none() }
3844
}
3945
}
4046

python/ql/lib/semmle/python/frameworks/Fabric.qll

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,19 @@ private module FabricV1 {
4343
* - https://docs.fabfile.org/en/1.14/api/core/operations.html#fabric.operations.run
4444
* - https://docs.fabfile.org/en/1.14/api/core/operations.html#fabric.operations.sudo
4545
*/
46-
private class FabricApiLocalRunSudoCall extends SystemCommandExecution::Range,
47-
DataFlow::CallCfgNode {
46+
private class FabricApiLocalRunSudoCall extends SystemCommandExecution::Range, API::CallNode {
4847
FabricApiLocalRunSudoCall() { this = api().getMember(["local", "run", "sudo"]).getACall() }
4948

5049
override DataFlow::Node getCommand() {
5150
result = [this.getArg(0), this.getArgByName("command")]
5251
}
52+
53+
override predicate isShellInterpreted(DataFlow::Node arg) {
54+
arg = this.getCommand() and
55+
// defaults to running in a shell
56+
not this.getParameter(1, "shell").asSink().asExpr().(ImmutableLiteral).booleanValue() =
57+
false // TODO: Test this in unsafe-shell-command-construction - and add tracking as a separate step.
58+
}
5359
}
5460
}
5561
}
@@ -161,6 +167,8 @@ private module FabricV2 {
161167
override DataFlow::Node getCommand() {
162168
result = [this.getArg(0), this.getArgByName("command")]
163169
}
170+
171+
override predicate isShellInterpreted(DataFlow::Node arg) { arg = this.getCommand() }
164172
}
165173

166174
// -------------------------------------------------------------------------
@@ -243,6 +251,8 @@ private module FabricV2 {
243251
override DataFlow::Node getCommand() {
244252
result = [this.getArg(0), this.getArgByName("command")]
245253
}
254+
255+
override predicate isShellInterpreted(DataFlow::Node arg) { arg = this.getCommand() }
246256
}
247257

248258
/**

python/ql/lib/semmle/python/frameworks/Invoke.qll

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,5 +81,7 @@ private module Invoke {
8181
override DataFlow::Node getCommand() {
8282
result in [this.getArg(0), this.getArgByName("command")]
8383
}
84+
85+
override predicate isShellInterpreted(DataFlow::Node arg) { arg = this.getCommand() }
8486
}
8587
}

python/ql/lib/semmle/python/frameworks/Stdlib.qll

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1060,7 +1060,11 @@ private module StdlibPrivate {
10601060
private class OsSystemCall extends SystemCommandExecution::Range, DataFlow::CallCfgNode {
10611061
OsSystemCall() { this = os().getMember("system").getACall() }
10621062

1063-
override DataFlow::Node getCommand() { result = this.getArg(0) }
1063+
override DataFlow::Node getCommand() {
1064+
result in [this.getArg(0), this.getArgByName("command")]
1065+
}
1066+
1067+
override predicate isShellInterpreted(DataFlow::Node arg) { arg = this.getCommand() }
10641068
}
10651069

10661070
/**
@@ -1071,7 +1075,7 @@ private module StdlibPrivate {
10711075
* Although deprecated since version 2.6, they still work in 2.7.
10721076
* See https://docs.python.org/2.7/library/os.html#os.popen2
10731077
*/
1074-
private class OsPopenCall extends SystemCommandExecution::Range, DataFlow::CallCfgNode {
1078+
private class OsPopenCall extends SystemCommandExecution::Range, API::CallNode {
10751079
string name;
10761080

10771081
OsPopenCall() {
@@ -1085,6 +1089,8 @@ private module StdlibPrivate {
10851089
not name = "popen" and
10861090
result = this.getArgByName("cmd")
10871091
}
1092+
1093+
override predicate isShellInterpreted(DataFlow::Node arg) { arg = this.getCommand() }
10881094
}
10891095

10901096
/**
@@ -1103,6 +1109,10 @@ private module StdlibPrivate {
11031109
override DataFlow::Node getCommand() { result = this.getArg(0) }
11041110

11051111
override DataFlow::Node getAPathArgument() { result = this.getCommand() }
1112+
1113+
override predicate isShellInterpreted(DataFlow::Node arg) {
1114+
none() // this is a safe API.
1115+
}
11061116
}
11071117

11081118
/**
@@ -1129,6 +1139,10 @@ private module StdlibPrivate {
11291139
}
11301140

11311141
override DataFlow::Node getAPathArgument() { result = this.getCommand() }
1142+
1143+
override predicate isShellInterpreted(DataFlow::Node arg) {
1144+
none() // this is a safe API.
1145+
}
11321146
}
11331147

11341148
/**
@@ -1142,6 +1156,10 @@ private module StdlibPrivate {
11421156
override DataFlow::Node getCommand() { result in [this.getArg(0), this.getArgByName("path")] }
11431157

11441158
override DataFlow::Node getAPathArgument() { result = this.getCommand() }
1159+
1160+
override predicate isShellInterpreted(DataFlow::Node arg) {
1161+
none() // this is a safe API.
1162+
}
11451163
}
11461164

11471165
/** An additional taint step for calls to `os.path.join` */
@@ -1186,6 +1204,7 @@ private module StdlibPrivate {
11861204
}
11871205

11881206
private boolean get_shell_arg_value() {
1207+
// TODO: API-node this thing - with added tests for unsafe-shell-command-construction
11891208
not exists(this.get_shell_arg()) and
11901209
result = false
11911210
or
@@ -1239,6 +1258,11 @@ private module StdlibPrivate {
12391258
)
12401259
)
12411260
}
1261+
1262+
override predicate isShellInterpreted(DataFlow::Node arg) {
1263+
arg = this.get_executable_arg() and
1264+
this.get_shell_arg_value() = true
1265+
}
12421266
}
12431267

12441268
// ---------------------------------------------------------------------------
@@ -1385,6 +1409,8 @@ private module StdlibPrivate {
13851409
}
13861410

13871411
override DataFlow::Node getCommand() { result in [this.getArg(0), this.getArgByName("cmd")] }
1412+
1413+
override predicate isShellInterpreted(DataFlow::Node arg) { arg = this.getCommand() }
13881414
}
13891415

13901416
// ---------------------------------------------------------------------------
@@ -1401,6 +1427,8 @@ private module StdlibPrivate {
14011427
PlatformPopenCall() { this = platform().getMember("popen").getACall() }
14021428

14031429
override DataFlow::Node getCommand() { result in [this.getArg(0), this.getArgByName("cmd")] }
1430+
1431+
override predicate isShellInterpreted(DataFlow::Node arg) { arg = this.getCommand() }
14041432
}
14051433

14061434
// ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)