зеркало из https://github.com/github/codeql.git
Merge pull request #3111 from RasmusWL/python-fabric-command-injection
Approved by BekaValentine
This commit is contained in:
Коммит
ac7c74dcee
|
@ -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
|
Загрузка…
Ссылка в новой задаче