Python: Add tests for all SystemCommandExecution from stdlib

Overall idea is that `test/experimental/meta/ConceptsTest.qll` will set up
inline expectation tests for all the classes defined in `Concepts.qll`, so any
time you model a new instance of Concepts, you simply just import that
file. That makes the tests a little verbose, but allows us to share test-setup
between all the different frameworks we model.

Note that since the definitions of SystemCommandExecution subclasses are
scattered across multieple framework modeling qll files, it think it makes the
most sense to have the tests for each framework in one location.

I'm not 100% convinced about if this is the right choice or not (especially when
we want to write tests for sanitizers), but for now I'm going to try it out at
least.
This commit is contained in:
Rasmus Wriedt Larsen
2020-09-24 18:16:49 +02:00
parent 20c4d94ccc
commit 060720aae7
4 changed files with 161 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
| SystemCommandExecution.py:16:10:16:21 | ControlFlowNode for Str | Fixed false negative:SystemCommandExecution_getCommand="cmd1; cmd2" |
| SystemCommandExecution.py:17:11:17:22 | ControlFlowNode for Str | Fixed false negative:SystemCommandExecution_getCommand="cmd1; cmd2" |
| SystemCommandExecution.py:27:11:27:22 | ControlFlowNode for Str | Fixed false negative:SystemCommandExecution_getCommand="cmd1; cmd2" |
| SystemCommandExecution.py:28:12:28:23 | ControlFlowNode for Str | Fixed false negative:SystemCommandExecution_getCommand="cmd1; cmd2" |

View File

@@ -0,0 +1,2 @@
import python
import experimental.meta.ConceptsTest

View File

@@ -0,0 +1,122 @@
# Note: Some of these commands will technically not allow an attacker to execute
# arbitrary system commands, but only specify the program to be executed. The general
# consensus was that even this is still a high security risk, so we also treat them as
# system command executions.
#
# As an example, executing `subprocess.Popen(["rm -rf /"])` will result in
# `FileNotFoundError: [Errno 2] No such file or directory: 'rm -rf /'`
########################################
import os
# can't use a string literal with spaces in the tags of an InlineExpectationsTest, so using variables :|
os.popen("cmd1; cmd2") # $f-:SystemCommandExecution_getCommand="cmd1; cmd2"
os.system("cmd1; cmd2") # $f-:SystemCommandExecution_getCommand="cmd1; cmd2"
def os_members():
# hmm, it's kinda annoying to check that we handle this import correctly for
# everything. It's quite useful since I messed it up initially and didn't have a
# test for it, but in the long run it's just cumbersome to duplicate all the tests
# :|
from os import popen, system
popen("cmd1; cmd2") # $f-:SystemCommandExecution_getCommand="cmd1; cmd2"
system("cmd1; cmd2") # $f-:SystemCommandExecution_getCommand="cmd1; cmd2"
########################################
# https://docs.python.org/3.8/library/os.html#os.execv
#
# VS Code extension will ignore rest of program if encountering one of these, which we
# don't want. We could use `if False`, but just to be 100% sure we don't do anything too
# clever in our analysis that discards that code, I used `if UNKNOWN` instead
if UNKNOWN:
env = {"FOO": "foo"}
os.execl("executable", "<progname>", "arg0") # $f-:SystemCommandExecution_getCommand="executable"
os.execle("executable", "<progname>", "arg0", env) # $f-:SystemCommandExecution_getCommand="executable"
os.execlp("executable", "<progname>", "arg0") # $f-:SystemCommandExecution_getCommand="executable"
os.execlpe("executable", "<progname>", "arg0", env) # $f-:SystemCommandExecution_getCommand="executable"
os.execv("executable", ["<progname>", "arg0"]) # $f-:SystemCommandExecution_getCommand="executable"
os.execve("executable", ["<progname>", "arg0"], env) # $f-:SystemCommandExecution_getCommand="executable"
os.execvp("executable", ["<progname>", "arg0"]) # $f-:SystemCommandExecution_getCommand="executable"
os.execvpe("executable", ["<progname>", "arg0"], env) # $f-:SystemCommandExecution_getCommand="executable"
########################################
# https://docs.python.org/3.8/library/os.html#os.spawnl
env = {"FOO": "foo"}
os.spawnl(os.P_WAIT, "executable", "<progname>", "arg0") # $f-:SystemCommandExecution_getCommand="executable"
os.spawnle(os.P_WAIT, "executable", "<progname>", "arg0", env) # $f-:SystemCommandExecution_getCommand="executable"
os.spawnlp(os.P_WAIT, "executable", "<progname>", "arg0") # $f-:SystemCommandExecution_getCommand="executable"
os.spawnlpe(os.P_WAIT, "executable", "<progname>", "arg0", env) # $f-:SystemCommandExecution_getCommand="executable"
os.spawnv(os.P_WAIT, "executable", ["<progname>", "arg0"]) # $f-:SystemCommandExecution_getCommand="executable"
os.spawnve(os.P_WAIT, "executable", ["<progname>", "arg0"], env) # $f-:SystemCommandExecution_getCommand="executable"
os.spawnvp(os.P_WAIT, "executable", ["<progname>", "arg0"]) # $f-:SystemCommandExecution_getCommand="executable"
os.spawnvpe(os.P_WAIT, "executable", ["<progname>", "arg0"], env) # $f-:SystemCommandExecution_getCommand="executable"
# Added in Python 3.8
os.posix_spawn("executable", ["<progname>", "arg0"], env) # $f-:SystemCommandExecution_getCommand="executable"
os.posix_spawnp("executable", ["<progname>", "arg0"], env) # $f-:SystemCommandExecution_getCommand="executable"
########################################
import subprocess
subprocess.Popen("cmd1; cmd2", shell=True) # $f-:SystemCommandExecution_getCommand="cmd1; cmd2"
subprocess.Popen("cmd1; cmd2", shell="truthy string") # $f-:SystemCommandExecution_getCommand="cmd1; cmd2"
subprocess.Popen(["cmd1; cmd2", "shell-arg"], shell=True) # $f-:SystemCommandExecution_getCommand="cmd1; cmd2"
subprocess.Popen("cmd1; cmd2", shell=True, executable="/bin/bash") # $f-:SystemCommandExecution_getCommand="cmd1; cmd2"
subprocess.Popen("executable") # $f-:SystemCommandExecution_getCommand="executable"
subprocess.Popen(["executable", "arg0"]) # $f-:SystemCommandExecution_getCommand="executable"
subprocess.Popen("<progname>", executable="executable") # $f-:SystemCommandExecution_getCommand="executable"
subprocess.Popen(["<progname>", "arg0"], executable="executable") # $f-:SystemCommandExecution_getCommand="executable"
# call/check_call/check_output/run all work like Popen from a command execution point of view
subprocess.call(["executable", "arg0"]) # $f-:SystemCommandExecution_getCommand="executable"
subprocess.check_call(["executable", "arg0"]) # $f-:SystemCommandExecution_getCommand="executable"
subprocess.check_output(["executable", "arg0"]) # $f-:SystemCommandExecution_getCommand="executable"
subprocess.run(["executable", "arg0"]) # $f-:SystemCommandExecution_getCommand="executable"
########################################
# actively using known shell as the executable
subprocess.Popen(["/bin/sh", "-c", "vuln"]) # $f-:SystemCommandExecution_getCommand="/bin/sh",$f-:SystemCommandExecution_getCommand="vuln"
subprocess.Popen(["/bin/bash", "-c", "vuln"]) # $f-:SystemCommandExecution_getCommand="/bin/bash",$f-:SystemCommandExecution_getCommand="vuln"
subprocess.Popen(["/bin/dash", "-c", "vuln"]) # $f-:SystemCommandExecution_getCommand="/bin/dash",$f-:SystemCommandExecution_getCommand="vuln"
subprocess.Popen(["/bin/zsh", "-c", "vuln"]) # $f-:SystemCommandExecution_getCommand="/bin/zsh",$f-:SystemCommandExecution_getCommand="vuln"
subprocess.Popen(["sh", "-c", "vuln"]) # $f-:SystemCommandExecution_getCommand="sh",$f-:SystemCommandExecution_getCommand="vuln"
subprocess.Popen(["bash", "-c", "vuln"]) # $f-:SystemCommandExecution_getCommand="bash",$f-:SystemCommandExecution_getCommand="vuln"
subprocess.Popen(["dash", "-c", "vuln"]) # $f-:SystemCommandExecution_getCommand="dash",$f-:SystemCommandExecution_getCommand="vuln"
subprocess.Popen(["zsh", "-c", "vuln"]) # $f-:SystemCommandExecution_getCommand="zsh",$f-:SystemCommandExecution_getCommand="vuln"
# Check that we don't consider ANY argument a command injection sink
subprocess.Popen(["sh", "/bin/python"]) # $f-:SystemCommandExecution_getCommand="sh"
subprocess.Popen(["cmd.exe", "/c", "vuln"]) # $f-:SystemCommandExecution_getCommand="cmd.exe",$f-:SystemCommandExecution_getCommand="vuln"
subprocess.Popen(["cmd.exe", "/C", "vuln"]) # $f-:SystemCommandExecution_getCommand="cmd.exe",$f-:SystemCommandExecution_getCommand="vuln"
subprocess.Popen(["cmd", "/c", "vuln"]) # $f-:SystemCommandExecution_getCommand="cmd",$f-:SystemCommandExecution_getCommand="vuln"
subprocess.Popen(["cmd", "/C", "vuln"]) # $f-:SystemCommandExecution_getCommand="cmd",$f-:SystemCommandExecution_getCommand="vuln"
################################################################################
# Taint related
import shlex
cmd = shlex.join(["echo", tainted])
args = shlex.split(tainted)
# will handle tainted = 'foo; rm -rf /'
safe_cmd = "ls {}".format(shlex.quote(tainted))
# not how you are supposed to use shlex.quote
wrong_use = shlex.quote("ls {}".format(tainted))
# still dangerous, for example
cmd = "sh -c " + wrong_use

View File

@@ -0,0 +1,33 @@
import python
import experimental.dataflow.DataFlow
import experimental.semmle.python.Concepts
import TestUtilities.InlineExpectationsTest
string value_from_expr(Expr e) {
// TODO: This one is starting to look like `repr` predicate from TestTaintLib
result =
e.(StrConst).getPrefix() + e.(StrConst).getText() +
e.(StrConst).getPrefix().regexpReplaceAll("[a-zA-Z]+", "")
or
result = e.(Name).getId()
or
not e instanceof StrConst and
not e instanceof Name and
result = e.toString()
}
class SystemCommandExecutionTest extends InlineExpectationsTest {
SystemCommandExecutionTest() { this = "SystemCommandExecutionTest" }
override string getARelevantTag() { result = "SystemCommandExecution_getCommand" }
override predicate hasActualResult(Location location, string element, string tag, string value) {
exists(SystemCommandExecution sce, DataFlow::Node command |
command = sce.getCommand() and
location = command.getLocation() and
element = command.toString() and
value = value_from_expr(command.asExpr()) and
tag = "SystemCommandExecution_getCommand"
)
}
}