Merge pull request #3111 from RasmusWL/python-fabric-command-injection

Approved by BekaValentine
This commit is contained in:
semmle-qlci 2020-03-25 10:07:33 +00:00 коммит произвёл GitHub
Родитель ae076da517 49fa7c8589
Коммит ac7c74dcee
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
18 изменённых файлов: 337 добавлений и 36 удалений

Просмотреть файл

@ -15,6 +15,7 @@ Support for Django version 2.x and 3.x
| **Query** | **Expected impact** | **Change** |
|----------------------------|------------------------|------------------------------------------------------------------|
| Uncontrolled command line (`py/command-line-injection`) | More results | We now model the `fabric` and `invoke` pacakges for command execution. |
### Web framework support

Просмотреть файл

@ -29,8 +29,7 @@ class CommandInjectionConfiguration extends TaintTracking::Configuration {
}
override predicate isSink(TaintTracking::Sink sink) {
sink instanceof OsCommandFirstArgument or
sink instanceof ShellCommand
sink instanceof CommandSink
}
override predicate isExtension(TaintTracking::Extension extension) {

Просмотреть файл

@ -1,15 +1,17 @@
/** Provides class and predicates to track external data that
/**
* Provides class and predicates to track external data that
* may represent malicious OS commands.
*
* This module is intended to be imported into a taint-tracking query
* to extend `TaintKind` and `TaintSink`.
*
*/
import python
import python
import semmle.python.security.TaintTracking
import semmle.python.security.strings.Untrusted
/** Abstract taint sink that is potentially vulnerable to malicious shell commands. */
abstract class CommandSink extends TaintSink { }
private ModuleObject osOrPopenModule() {
result.getName() = "os" or
@ -17,10 +19,9 @@ private ModuleObject osOrPopenModule() {
}
private Object makeOsCall() {
exists(string name |
result = ModuleObject::named("subprocess").attr(name) |
exists(string name | result = ModuleObject::named("subprocess").attr(name) |
name = "Popen" or
name = "call" or
name = "call" or
name = "check_call" or
name = "check_output" or
name = "run"
@ -29,40 +30,27 @@ private Object makeOsCall() {
/**Special case for first element in sequence. */
class FirstElementKind extends TaintKind {
FirstElementKind() { this = "sequence[" + any(ExternalStringKind key) + "][0]" }
FirstElementKind() {
this = "sequence[" + any(ExternalStringKind key) + "][0]"
}
override string repr() {
result = "first item in sequence of " + this.getItem().repr()
}
override string repr() { result = "first item in sequence of " + this.getItem().repr() }
/** Gets the taint kind for item in this sequence. */
ExternalStringKind getItem() {
this = "sequence[" + result + "][0]"
}
ExternalStringKind getItem() { this = "sequence[" + result + "][0]" }
}
class FirstElementFlow extends DataFlowExtension::DataFlowNode {
FirstElementFlow() { this = any(SequenceNode s).getElement(0) }
FirstElementFlow() {
this = any(SequenceNode s).getElement(0)
}
override
ControlFlowNode getASuccessorNode(TaintKind fromkind, TaintKind tokind) {
override ControlFlowNode getASuccessorNode(TaintKind fromkind, TaintKind tokind) {
result.(SequenceNode).getElement(0) = this and tokind.(FirstElementKind).getItem() = fromkind
}
}
/** A taint sink that is potentially vulnerable to malicious shell commands.
/**
* A taint sink that is potentially vulnerable to malicious shell commands.
* The `vuln` in `subprocess.call(shell=vuln)` and similar calls.
*/
class ShellCommand extends TaintSink {
class ShellCommand extends CommandSink {
override string toString() { result = "shell command" }
ShellCommand() {
@ -75,7 +63,8 @@ class ShellCommand extends TaintSink {
or
exists(CallNode call, string name |
call.getAnArg() = this and
call.getFunction().refersTo(osOrPopenModule().attr(name)) |
call.getFunction().refersTo(osOrPopenModule().attr(name))
|
name = "system" or
name = "popen" or
name.matches("popen_")
@ -94,19 +83,18 @@ class ShellCommand extends TaintSink {
/* List (or tuple) containing a tainted string command */
kind instanceof ExternalStringSequenceKind
}
}
/** A taint sink that is potentially vulnerable to malicious shell commands.
/**
* A taint sink that is potentially vulnerable to malicious shell commands.
* The `vuln` in `subprocess.call(vuln, ...)` and similar calls.
*/
class OsCommandFirstArgument extends TaintSink {
class OsCommandFirstArgument extends CommandSink {
override string toString() { result = "OS command first argument" }
OsCommandFirstArgument() {
not this instanceof ShellCommand and
exists(CallNode call|
exists(CallNode call |
call.getFunction().refersTo(makeOsCall()) and
call.getArg(0) = this
)
@ -119,5 +107,127 @@ class OsCommandFirstArgument extends TaintSink {
/* List (or tuple) whose first element is tainted */
kind instanceof FirstElementKind
}
}
// -------------------------------------------------------------------------- //
// Modeling of the 'invoke' package and 'fabric' package (v 2.x)
//
// Since fabric build so closely upon invoke, we model them together to avoid
// duplication
// -------------------------------------------------------------------------- //
/**
* A taint sink that is potentially vulnerable to malicious shell commands.
* The `vuln` in `invoke.run(vuln, ...)` and similar calls.
*/
class InvokeRun extends CommandSink {
InvokeRun() {
this = Value::named("invoke.run").(FunctionValue).getArgumentForCall(_, 0)
or
this = Value::named("invoke.sudo").(FunctionValue).getArgumentForCall(_, 0)
}
override string toString() { result = "InvokeRun" }
override predicate sinks(TaintKind kind) { kind instanceof ExternalStringKind }
}
/**
* Internal TaintKind to track the invoke.Context instance passed to functions
* marked with @invoke.task
*/
private class InvokeContextArg extends TaintKind {
InvokeContextArg() { this = "InvokeContextArg" }
}
/** Internal TaintSource to track the context passed to functions marked with @invoke.task */
private class InvokeContextArgSource extends TaintSource {
InvokeContextArgSource() {
exists(Function f, Expr decorator |
count(f.getADecorator()) = 1 and
(
decorator = f.getADecorator() and not decorator instanceof Call
or
decorator = f.getADecorator().(Call).getFunc()
) and
(
decorator.pointsTo(Value::named("invoke.task"))
or
decorator.pointsTo(Value::named("fabric.task"))
)
|
this.(ControlFlowNode).getNode() = f.getArg(0)
)
}
override predicate isSourceOf(TaintKind kind) { kind instanceof InvokeContextArg }
}
/**
* A taint sink that is potentially vulnerable to malicious shell commands.
* The `vuln` in `invoke.Context().run(vuln, ...)` and similar calls.
*/
class InvokeContextRun extends CommandSink {
InvokeContextRun() {
exists(CallNode call |
any(InvokeContextArg k).taints(call.getFunction().(AttrNode).getObject("run"))
or
call = Value::named("invoke.Context").(ClassValue).lookup("run").getACall()
or
// fabric.connection.Connection is a subtype of invoke.context.Context
// since fabric.Connection.run has a decorator, it doesn't work with FunctionValue :|
// and `Value::named("fabric.Connection").(ClassValue).lookup("run").getACall()` returned no results,
// so here is the hacky solution that works :\
call.getFunction().(AttrNode).getObject("run").pointsTo().getClass() =
Value::named("fabric.Connection")
|
this = call.getArg(0)
or
this = call.getArgByName("command")
)
}
override string toString() { result = "InvokeContextRun" }
override predicate sinks(TaintKind kind) { kind instanceof ExternalStringKind }
}
/**
* A taint sink that is potentially vulnerable to malicious shell commands.
* The `vuln` in `fabric.Group().run(vuln, ...)` and similar calls.
*/
class FabricGroupRun extends CommandSink {
FabricGroupRun() {
exists(ClassValue cls |
cls.getASuperType() = Value::named("fabric.Group") and
this = cls.lookup("run").(FunctionValue).getArgumentForCall(_, 1)
)
}
override string toString() { result = "FabricGroupRun" }
override predicate sinks(TaintKind kind) { kind instanceof ExternalStringKind }
}
// -------------------------------------------------------------------------- //
// Modeling of the 'invoke' package and 'fabric' package (v 1.x)
// -------------------------------------------------------------------------- //
class FabricV1Commands extends CommandSink {
FabricV1Commands() {
// since `run` and `sudo` are decorated, we can't use FunctionValue's :(
exists(CallNode call |
call = Value::named("fabric.api.local").getACall()
or
call = Value::named("fabric.api.run").getACall()
or
call = Value::named("fabric.api.sudo").getACall()
|
this = call.getArg(0)
or
this = call.getArgByName("command")
)
}
override string toString() { result = "FabricV1Commands" }
override predicate sinks(TaintKind kind) { kind instanceof ExternalStringKind }
}

Просмотреть файл

@ -0,0 +1,17 @@
| fabric_v1_test.py:8:7:8:28 | FabricV1Commands | externally controlled string |
| fabric_v1_test.py:9:5:9:27 | FabricV1Commands | externally controlled string |
| fabric_v1_test.py:10:6:10:38 | FabricV1Commands | externally controlled string |
| fabric_v2_test.py:10:16:10:25 | InvokeContextRun | externally controlled string |
| fabric_v2_test.py:12:15:12:36 | InvokeContextRun | externally controlled string |
| fabric_v2_test.py:16:45:16:54 | FabricGroupRun | externally controlled string |
| fabric_v2_test.py:21:10:21:13 | FabricGroupRun | externally controlled string |
| fabric_v2_test.py:31:14:31:41 | InvokeContextRun | externally controlled string |
| fabric_v2_test.py:33:15:33:64 | InvokeContextRun | externally controlled string |
| invoke_test.py:8:12:8:21 | InvokeRun | externally controlled string |
| invoke_test.py:9:20:9:40 | InvokeRun | externally controlled string |
| invoke_test.py:12:17:12:24 | InvokeRun | externally controlled string |
| invoke_test.py:13:25:13:32 | InvokeRun | externally controlled string |
| invoke_test.py:17:11:17:40 | InvokeContextRun | externally controlled string |
| invoke_test.py:21:11:21:32 | InvokeContextRun | externally controlled string |
| invoke_test.py:27:11:27:25 | InvokeContextRun | externally controlled string |
| invoke_test.py:32:11:32:25 | InvokeContextRun | externally controlled string |

Просмотреть файл

@ -0,0 +1,7 @@
import python
import semmle.python.security.injection.Command
import semmle.python.security.strings.Untrusted
from CommandSink sink, TaintKind kind
where sink.sinks(kind)
select sink, kind

Просмотреть файл

@ -0,0 +1,22 @@
Copyright (c) 2020 Jeff Forcier.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Просмотреть файл

@ -0,0 +1,10 @@
"""tests for the 'fabric' package (v1.x)
See http://docs.fabfile.org/en/1.14/tutorial.html
"""
from fabric.api import run, local, sudo
local('echo local execution')
run('echo remote execution')
sudo('echo remote execution with sudo')

Просмотреть файл

@ -0,0 +1,33 @@
"""tests for the 'fabric' package (v2.x)
Most of these examples are taken from the fabric documentation: http://docs.fabfile.org/en/2.5/getting-started.html
See fabric-LICENSE for its' license.
"""
from fabric import Connection
c = Connection('web1')
result = c.run('uname -s')
c.run(command='echo run with kwargs')
from fabric import SerialGroup as Group
results = Group('web1', 'web2', 'mac1').run('uname -s')
from fabric import SerialGroup as Group
pool = Group('web1', 'web2', 'web3')
pool.run('ls')
# using the 'fab' command-line tool
from fabric import task
@task
def upload_and_unpack(c):
if c.run('test -f /opt/mydata/myfile', warn=True).failed:
c.put('myfiles.tgz', '/opt/mydata')
c.run('tar -C /opt/mydata -xzvf /opt/mydata/myfiles.tgz')

Просмотреть файл

@ -0,0 +1,32 @@
"""tests for the 'invoke' package
see https://www.pyinvoke.org/
"""
import invoke
invoke.run('echo run')
invoke.run(command='echo run with kwarg')
def with_sudo():
invoke.sudo('whoami')
invoke.sudo(command='whoami')
def manual_context():
c = invoke.Context()
c.run('echo run from manual context')
manual_context()
def foo_helper(c):
c.run('echo from foo_helper')
# for use with the 'invoke' command-line tool
@invoke.task
def foo(c):
# 'c' is a invoke.context.Context
c.run('echo task foo')
foo_helper(c)
@invoke.task()
def bar(c):
c.run('echo task bar')

Просмотреть файл

@ -0,0 +1 @@
semmle-extractor-options: --max-import-depth=2 -p ../../../query-tests/Security/lib/

Просмотреть файл

@ -0,0 +1,3 @@
from .connection import Connection
from .group import Group, SerialGroup, ThreadingGroup
from .tasks import task

Просмотреть файл

@ -0,0 +1,25 @@
# For the 1.x version
def needs_host(func):
@wraps(func)
def inner(*args, **kwargs):
return func(*args, **kwargs)
return inner
def local(command, capture=False, shell=None):
pass
@needs_host
def run(command, shell=True, pty=True, combine_stderr=None, quiet=False,
warn_only=False, stdout=None, stderr=None, timeout=None, shell_escape=None,
capture_buffer_size=None):
pass
@needs_host
def sudo(command, shell=True, pty=True, combine_stderr=None, user=None,
quiet=False, warn_only=False, stdout=None, stderr=None, group=None,
timeout=None, shell_escape=None, capture_buffer_size=None):
pass

Просмотреть файл

@ -0,0 +1,15 @@
from invoke import Context
@decorator
def opens(method, self, *args, **kwargs):
self.open()
return method(self, *args, **kwargs)
class Connection(Context):
def open(self):
pass
@opens
def run(self, command, **kwargs):
pass

Просмотреть файл

@ -0,0 +1,11 @@
class Group(list):
def run(self, *args, **kwargs):
raise NotImplementedError
class SerialGroup(Group):
def run(self, *args, **kwargs):
pass
class ThreadingGroup(Group):
def run(self, *args, **kwargs):
pass

Просмотреть файл

@ -0,0 +1,2 @@
def task(*args, **kwargs):
pass

Просмотреть файл

@ -0,0 +1,8 @@
from .context import Context
from .tasks import task
def run(command, **kwargs):
pass
def sudo(command, **kwargs):
pass

Просмотреть файл

@ -0,0 +1,3 @@
class Context(object):
def run(self, command, **kwargs):
pass

Просмотреть файл

@ -0,0 +1,2 @@
def task(*args, **kwargs):
pass